今天同步下最近在做的一个AI项目技术笔记: AI数字人名片编辑器。
国庆中秋双节期间注册就送 2000在信豆。
在线体验地址:xn--gmqy7wt0j.zaixin.com/main/home
项目遵循了现代前端开发中的数据驱动和单一数据源 (Single Source of Truth) 原则。所有的元素位置、尺寸(包括数字人形象、Logo、文本等)在 Pinia Store (aiCard.js) 中都以原始模板尺寸的像素值进行存储。
核心逻辑流程如下:
- 数据初始化: 后端返回的
templateData包含所有元素的原始尺寸和位置信息。 - 基准确立: 底图加载完成后,捕获其原始尺寸 (
originalWidth,originalHeight) 和当前的显示尺寸 (backgroundWidth,backgroundHeight)。 - 计算缩放: 通过
scaleRatioGetter 计算出显示尺寸与原始尺寸的比例。 - 视图渲染: 所有元素的样式在渲染时,都通过
getStyleValueAction,利用scaleRatio将原始值动态转换为屏幕显示值。
// aiCard.js 中的 scaleRatio Getter
scaleRatio: (state) => {
if (state.originalHeight === 0) return 1
// 采用高度比作为基准缩放比,确保元素与底图同步缩放
return state.backgroundHeight / state.originalHeight;
}
// aiCard.js 中的 getStyleValue Action
getStyleValue(prop, value, ratio) {
const numericValue = parseFloat(value);
// 只有带px单位的属性才进行缩放
if (isNaN(numericValue) || prop.indexOf('px') === -1) {
return value;
}
// value 为原始值,ratio 为 scaleRatio
return numericValue * ratio;
}
这种设计保证了用户在编辑器中对元素进行的操作(拖拽、缩放)实际上修改的是 Store 中的原始值,而视图只负责将这些原始值按比例显示,完美实现了数据与视图的解耦。
二、固定底图尺寸和缩放比:基准的建立
要使所有元素都以底图为基准进行缩放,首先必须固定底图的显示尺寸和比例。
1. 底图样式的关键实现
在 editContainer.vue 中,底图的父容器 .image-wrapper 负责决定最大可用空间,而底图自身通过以下 CSS 确保自动适应父容器并保持比例:
/* editContainer.vue style scoped */
.image-wrapper {
position: relative;
overflow: hidden;
height: 100%; /* 父容器高度占满 */
/* ... 其他样式 */
}
.background {
height: 100%; /* 图片高度自动撑满父容器 image-wrapper 的高度 */
display: block;
width: auto; /* 宽度按比例自动调整 */
position: relative;
z-index: 11;
}
通过设置底图 <img class="background" /> 的 height: 100%; 和 width: auto;,底图会垂直撑满编辑容器的可用空间,并且宽度自动等比例缩放。这样,底图的显示尺寸就确定了,成为了所有元素缩放的基准。底图加载完成后,handleImageLoad 记录下其 naturalWidth/Height(原始尺寸)和 backgroundWidth/Height(显示尺寸),为 scaleRatio 的计算提供了基础。
2. 精确计算图片比例的核心函数
为了精确地处理图片的等比例缩放,我们需要知道其最简整数比。这通过 getImageAspectRatio 和 getGCD 两个工具函数实现:
// aiCard.js 中的 getGCD
getGCD(a, b) {
// 递归计算最大公约数 (Greatest Common Divisor)
return b === 0 ? a : getGCD(b, a % b);
}
// aiCard.js 中的 getImageAspectRatio
function getImageAspectRatio(url) {
// ... 省略加载图片和获取 naturalWidth/Height 逻辑
// 计算最大公约数
const gcd = getGCD(width, height);
// 计算最简整数比
const wRatio = width / gcd; // 宽度比
const hRatio = height / gcd; // 高度比
resolve([wRatio, hRatio]);
}
为什么要计算最大公约数 (GCD)?
getGCD 的作用是将图片真实的像素尺寸(例如 1920x1080)简化为最简整数比(例如 16:9)。使用最简整数比进行后续的尺寸计算,可以确保在进行复杂的缩放和尺寸计算时,使用最简洁、最精确的比例值,避免因浮点数累积误差导致的微小偏差,确保等比例计算的绝对准确性。
三、数字人形象的底层置入与位置大小操控
1. 底层置入与叠放次序
数字人形象 (humanImage.vue 用于显示,humanEditor.vue 用于编辑) 被放置在底图 <img class="background"> 的附近,且都处于 .image-wrapper 容器中。
- 底图在
editContainer.vue中设置z-index: 11;。 - 数字人编辑器在
humanEditor.vue中设置position: absolute;和z-index: 111;。
这种设计使得底图作为画布的底层,而数字人编辑器则绝对定位并覆盖在底图之上,且具有更高的 z-index,从而可以接收鼠标事件进行拖拽和缩放操作。human-editor 的 cursor: move; 也明确了其可被直接操控的特性。
2. 操控与数据同步
用户在 humanEditor.vue 中进行拖拽和缩放操作时:
handleDragStart和handleResizeStart捕获鼠标事件。handleMouseMove和handleResize实时计算新的位置 (left,top) 和尺寸 (width,height)。- 关键在于,
humanEditor.vue计算出的新值是基于当前显示尺寸的像素值,但它会将其逆向转换回原始模板尺寸的值,并更新到 Pinia Store 的humanData中。 - 随后,
humanImage.vue中的watch会监测到 Store 的变化,并重新计算缩放后的AICardStore.humanStyle,并将样式应用到humanEditor.vue上,完成闭环。
四、保证任意形象在操控情况下不变形
不变形(即保持图片的固有宽高比)是数字人形象编辑的核心要求。这一机制主要在 humanImage.vue 的 watch 逻辑中实现。
1. 核心逻辑
humanImage.vue 通过监听 AICardStore.humanData 的变化,执行以下逻辑:
- 获取数字人图片的最简宽高比
ratio(e.g.,[wRatio, hRatio]),这来自于getImageAspectRatio。 - 获取 Store 中存储的原始尺寸(
props.style.width,props.style.height)。 - 计算显示尺寸:先按
scaleRatio计算出其中一个维度的显示值(例如width)。
2. 严格的比例保持
为了不变形,另一维度(height)不会直接使用原始值缩放后的结果,而是利用数字人图片的固有比例 ratio 来重新计算:
// humanImage.vue watch 逻辑片段
// ... 获取比例 ratio [wRatio, hRatio]
// 无论用户如何拖拽 humanEditor,这里都会强制重新计算 height 以保持比例
width = AICardStore.getStyleValue('width', props.style.width, props.scaleRatio)
// 注:这里应根据实际需求决定以哪个尺寸为基准。
// 假设以宽度为基准计算高度,确保比例:
height = `${width * (ratio[1] / ratio[0])}`; // 高度 = 宽度 * (hRatio / wRatio)
// ... 更新 AICardStore.humanStyle
在实际代码片段中,虽然存在一个 isVertical 的判断来决定以哪个方向(宽或高)的缩放作为基准,但核心思想始终是:
当数字人编辑器的尺寸发生变化时,我们总是用数字人形象自身的原始比例 ratio 来强制校正另一个维度,从而抵消用户在编辑框中可能引入的形变,确保数字人形象在任何操控下都能以正确的宽高比显示。
五、导出时恢复原尺寸的策略与技术
导出的目标是获取一个原始分辨率的图像文件,而非一个缩放后的屏幕截图。
1. 恢复原始尺寸的原理
如前所述,Store 中存储的都是原始尺寸值。导出的核心步骤是:
- 临时切换渲染尺寸: 在导出前,将包裹所有元素的
.image-wrapper容器的尺寸(宽度和高度)临时设置为AICardStore.originalWidth和AICardStore.originalHeight。 - 重置缩放比: 强制将
scaleRatio的计算结果视为1(或者将用于导出的scaleRatio参数设置为1)。 - 原始渲染: 此时,所有元素样式通过
getStyleValue(..., 1)计算出的值就是其原始像素值,整个卡片将在屏幕上以其原始分辨率进行渲染。
2. getCardImageFile 的技术要点(推断)
虽然 getCardImageFile 方法体未提供,但根据要求,其技术路线应为:DOM → SVG → Blob。
- DOM → SVG/Canvas 转换: 通常利用
html2canvas或dom-to-image等库将渲染完成的 DOM 结构(即恢复原始尺寸后的.image-wrapper元素)转换为 Canvas 或 SVG 格式。 - 为何要先转为 SVG 再转为 Blob (或 Canvas - > Blob):
-
- SVG 优势: SVG 是一种基于 XML 的矢量图形格式,尤其适合处理文本和几何图形,能保证导出的清晰度和精确度。
- Blob 是最终文件格式: Blob (Binary Large Object) 是一种存储二进制数据的对象。无论是将 SVG 或 Canvas 转换为图像文件(如 PNG/JPEG),最终都需要通过
toBlob()或类似方法将其封装成 Blob 对象,才能进行下载或上传操作。
3. 设置 pixelRatio=1 的必要性
在调用 html2canvas 或 dom-to-image 这类导出库时,必须设置 pixelRatio=1:
- 默认行为: 这些库默认会获取当前设备的像素比(
window.devicePixelRatio),例如在 Retina 屏上可能为2或3。 - 影响: 如果不设置
pixelRatio=1,导出的图片分辨率将会是原始尺寸 * devicePixelRatio,例如1920x1080的模板在2倍屏上会导出为3840x2160的图片。 - 目的: 设置
pixelRatio=1可以阻止导出库使用系统分辨率进行超采样,确保最终导出的文件分辨率精确等于我们设置的原始模板尺寸 (originalWidthxoriginalHeight),满足了导出文件尺寸的标准化需求。
六、保证数字人始终有一边小于模板尺寸
这个要求是针对数字人形象的最大尺寸约束。这需要在 humanEditor.vue 的 handleResize 逻辑中实施边界检查和钳制 (Clamping) 。
实现思路:
- 在用户拖拽角点进行缩放时,
handleResize实时计算出新的未缩放的newWidth和newHeight。 - 获取模板的原始尺寸
templateWidth(AICardStore.originalWidth) 和templateHeight(AICardStore.originalHeight)。 - 约束判断: 检查数字人新尺寸的两个维度是否同时大于或等于模板的相应维度。
// humanEditor.vue - 假想的 handleResize 逻辑片段
// newOriginalWidth/Height 是计算出的未缩放的新尺寸
const templateWidth = AICardStore.originalWidth;
const templateHeight = AICardStore.originalHeight;
// 1. 获取数字人自身的宽高比 (wRatio, hRatio)
const [wRatio, hRatio] = AICardStore.humanImageRatio;
// 2. 检查是否同时超出了模板的两个维度
// 确保新尺寸不会使得数字人完全覆盖或超出模板
if (newOriginalWidth > templateWidth && newOriginalHeight > templateHeight) {
// 如果同时超出,则需要根据比例进行钳制
// 通常是限制以模板的最短边为基准,或直接阻止进一步放大
// 钳制逻辑:限制最大尺寸为模板尺寸,并保持比例
const maxScaleByWidth = templateWidth / wRatio;
const maxScaleByHeight = templateHeight / hRatio;
// 选择一个较小的缩放基准,确保有一边小于模板
// 例如:当数字人缩放尺寸超过模板任一边时,进行比例缩小
// 此处需要复杂的边界计算确保合规。最简逻辑为:
if (newOriginalWidth > templateWidth) {
// 如果宽度超出模板宽度,将宽度限制为模板宽度,高度按比例回算
newOriginalWidth = templateWidth;
newOriginalHeight = templateWidth * (hRatio / wRatio);
} else if (newOriginalHeight > templateHeight) {
// 如果高度超出模板高度,将高度限制为模板高度,宽度按比例回算
newOriginalHeight = templateHeight;
newOriginalWidth = templateHeight * (wRatio / hRatio);
}
}
// ... 应用新的 newOriginalWidth/Height 到 Store
通过上述的边界钳制逻辑,数字人的尺寸被严格限制在模板尺寸之内,确保其在保持自身比例的同时,始终有一边(或两边)小于或等于模板的尺寸,从而满足了约束要求。
总结:
这套数字人卡片编辑器代码以 Pinia Store 为核心,通过原始数据存储和统一缩放比 (scaleRatio) 机制,优雅地解决了前端编辑器的核心难题:跨分辨率下的高保真实时渲染与高精度原始尺寸导出。从底图的自适应样式,到利用 GCD 保证比例的绝对精确,再到对数字人形象的尺寸钳制,每一个细节都体现了对性能、精度和用户体验的深刻理解。