Vue3中使用konva插件动态制作海报以及可在画布上随意移动位置

1、下载konva插件
官网地址

npm install vue-konva konva --save

2、在主文件中引入,如main.js

import VueKonva from 'vue-konva';
app.use(VueKonva);

3、组件内使用,我现在的布局是左侧是画布,右侧是相关设置(颜色、标题等)

  <div class="share_poster_wrap"><div class="poster_image_wrap"><div class="poster_img_left"><div class="loading-mask" v-if="loading"></div><div class="custom_loading" v-if="loading"><div class="loading_spinner"></div><p>AI生成背景中,请稍等...</p></div><p class="poster_img_tips"><Icon icon="svg-icon:tips" :size="14" />可以拖拽调整海报上任意元素的位置</p><div class="poster_img"><v-stageref="stage":config="stageSize"@dragmove="handleDragMove":key="stageKey"style="border-radius: 12px; overflow: hidden"><v-layer ref="layer"><v-rect:config="{width: stageSize.width,height: stageSize.height,fill: '#fff',listening: false,stroke: 'transparent',cornerRadius: 12,}"/><v-image :config="bgImageConfig" name="image" /><v-text :config="formTitleConfig" name="formTitleText" /><v-group :config="qrGroupConfig" name="qrGroup"><v-rect :config="qrConfig" name="qr" /><v-rect :config="qrLogoWrapConfig" name="qrLogo" /><v-rect :config="qrLogoConfig" name="qrLogo" /></v-group><v-rect :config="logoConfig" name="logo" /><v-circle :config="avatarConfig" name="circle" /><v-text :config="inviteConfig" name="inviteText" /><v-text :config="nickNameConfig" name="nickNameText" /><v-text :config="tipsConfig" name="tipsText" /></v-layer></v-stage></div><el-button @click="downloadImage" class="download_btn">下载海报</el-button></div><div class="poster_img_right"></div></div>

已上是我的html部分,左侧就是我的海报部分,是有头像标题和二维码以及背景,大家可以根据需求来变动。

const stageSize = {width: 340,height: 460,background: '#fff',
};// 背景图片配置
const bgImageConfig: any = ref({width: stageSize.width,height: stageSize.height,image: null, // 先初始化为空draggable: true,opacity: 1, // 确保不透明cornerRadius: [0, 0, 0, 0],// cornerRadius: 12,
});// 二维码组配置
const qrGroupConfig: any = ref({width: 136,height: 136,fill: '#fff',x: 100,y: 232,draggable: true,stroke: 'transparent',shadowEnabled: false,perfectDrawEnabled: false,
});// 二维码
const qrConfig: any = ref({width: 136,height: 136,cornerRadius: 6,x: 0,y: 0,
});// 二维码中间的logo外层
const qrLogoWrapConfig: any = ref({width: 30,height: 30,x: unref(qrConfig).x + (unref(qrConfig).width - 30) / 2,y: unref(qrConfig).y + (unref(qrConfig).height - 30) / 2,fill: '#fff',
});// 二维码中间的logo
const qrLogoConfig: any = ref({width: 30,height: 30,x: unref(qrLogoWrapConfig).x + (unref(qrLogoWrapConfig).width - 30) / 2,y: unref(qrLogoWrapConfig).y + (unref(qrLogoWrapConfig).height - 30) / 2,
});// 全局logo
const logoConfig: any = ref({width: 50,height: 50,x: 14,y: 14,fill: 'transparent',draggable: true,
});// 头像
const avatarConfig: any = ref({width: 50,height: 50,x: stageSize.width / 2,y: 80,radius: 25,draggable: true,stroke: '#fff',strokeWidth: 2,
});// 标题
const formTitleConfig: any = ref({text: '',fontSize: 16,x: stageSize.width / 2,y: 186,draggable: true,fontStyle: 'bold',dragged: false, // 是否被拖拽过
});// 邀请
const inviteConfig: any = ref({text: '',fontSize: 14,x: stageSize.width / 2,y: 144,draggable: true,dragged: false, // 是否被拖拽过
});// 昵称
const nickNameConfig: any = ref({text: '',fontSize: 16,x: stageSize.width / 2,y: 116,draggable: true,dragged: false, // 是否被拖拽过
});// 邀请文案
const tipsConfig: any = ref({text: '',fontSize: 14,x: stageSize.width / 2,y: 384,draggable: true,dragged: false, // 是否被拖拽过
});const stage = ref(); // 获取stage引用class DrawPosterImage {// 通用图片加载方法private async loadImage(url: string): Promise<HTMLImageElement | null> {if (!url) return null;try {const img = new Image();img.crossOrigin = 'Anonymous';return await new Promise((resolve, reject) => {img.onload = () => resolve(img);img.onerror = () => reject(null);img.src = url;if (img.complete) resolve(img);});} catch {return null;}}// 通用配置更新方法private updateConfig<T extends object>(configRef: Ref<T>, updates: Partial<T>) {configRef.value = { ...configRef.value, ...updates };}// 通用图片绘制方法private async drawImage(configRef: Ref<any>,url: string,options: {width?: number;height?: number;cornerRadius?: number;maintainAspectRatio?: boolean;} = {},) {if (!url) {this.updateConfig(configRef, {fill: 'transparent',fillPatternImage: undefined,});return;}const img = await this.loadImage(url);if (!img) {this.updateConfig(configRef, {fill: 'transparent',fillPatternImage: undefined,});return;}const updates: any = {fill: undefined,fillPatternImage: img,fillPatternRepeat: 'no-repeat',};if (options.maintainAspectRatio !== false) {const containerWidth = options.width || configRef.value.width;const containerHeight = options.height || configRef.value.height;const scale = Math.min(containerWidth / img.width, containerHeight / img.height);updates.fillPatternScale = { x: scale, y: scale };// 计算居中偏移const offsetX = (containerWidth - img.width * scale) / 2;const offsetY = (containerHeight - img.height * scale) / 2;updates.fillPatternOffset = {x: -offsetX / scale,y: -offsetY / scale,};}if (options.cornerRadius !== undefined) {updates.cornerRadius = options.cornerRadius;}this.updateConfig(configRef, updates);}public getDataUrl(): string | undefined {const stageNode = stage.value?.getStage();if (!stageNode) return;const dataURL = stageNode.toDataURL({pixelRatio: 1,mimeType: 'image/png',x: 0,y: 0,width: stageSize.width,height: stageSize.height,});posterImgUrl.value = dataURL;posterUrl.value = dataURL;return dataURL;}public async drawBackground(url: string, preserveColors = false): Promise<void> {const img = await this.loadImage(url);if (img) {const bgHeight = img.height > 460 ? 460 : img.height;this.updateConfig(bgImageConfig, {image: img,width: stageSize.width,height: bgHeight,});if (!preserveColors) {this.updateConfig(formTitleConfig, { fill: '#333333' });this.updateConfig(nickNameConfig, { fill: '#666666' });this.updateConfig(inviteConfig, { fill: '#666666' });this.updateConfig(tipsConfig, { fill: '#666666' });}}}public async drawCreaterAvatar(url = ''): Promise<void> {const img = await this.loadImage(url);if (img) {circleImg(img, stageSize.width / 2, 80);} else {this.updateConfig(avatarConfig, {fillPatternImage: undefined,stroke: 'transparent',strokeWidth: 2,});}}public drawCreaterNickName(nickName: string): void {this.updateConfig(nickNameConfig, {text: nickName,});}public drawInvitation(msg: string): void {this.updateConfig(inviteConfig, {text: msg,});}public async drawQRCode(url = '', isChange = false): Promise<void> {const img = await this.loadImage(url);if (img) {const scale = Math.min(qrConfig.value.width / img.width, qrConfig.value.height / img.height);this.updateConfig(qrConfig, {fillPatternImage: img,fillPatternScale: { x: scale, y: scale },fillPatternOffset: { x: 0, y: 0 },fillPatternRepeat: 'no-repeat',});if (!isChange) {this.updateConfig(tipsConfig, {text: '长按扫码进行填写',});}const logoUrl =unref(qrCodeLogo).length > 0 && unref(showCustomLogo)? unref(qrCodeLogo): unref(defaultInnerLogo);if (logoUrl) {const isType1 = unref(qrcodeType) === 1;const logoWrapSize = isType1 ? 30 : 60;const logoSize = isType1 ? 25 : 60;const cornerRadius = isType1 ? 5 : 50;await this.drawImage(qrLogoConfig, logoUrl, {width: logoSize,height: logoSize,cornerRadius,maintainAspectRatio: true,});this.updateConfig(qrLogoWrapConfig, {width: logoWrapSize,height: logoWrapSize,x: unref(qrConfig).x + (unref(qrConfig).width - logoWrapSize) / 2,y: unref(qrConfig).y + (unref(qrConfig).height - logoWrapSize) / 2,cornerRadius,});this.updateConfig(qrLogoConfig, {width: logoSize,height: logoSize,x: unref(qrConfig).x + (unref(qrConfig).width - logoSize) / 2,y: unref(qrConfig).y + (unref(qrConfig).height - logoSize) / 2,});}}}public async drawLogoImg(url = ''): Promise<void> {await this.drawImage(logoConfig, url, {maintainAspectRatio: true,});}public drawFormTitle(title = ''): void {const text = title.length > 30 ? `${title.substring(0, 30)}...` : title;this.updateConfig(formTitleConfig, {text,fontSize: 16,});}public cacheNodes() {const stageNode = stage.value?.getStage();if (!stageNode) return;// 缓存所有可拖动元素stageNode.find('Group').forEach((group) => group.cache());stageNode.find('Text').forEach((text) => text.cache());stageNode.find('Image').forEach((img) => img.cache());}
}// 文本默认居中
const updateTextPositions = () => {nextTick(() => {const stageNode = stage.value?.getStage();if (!stageNode) return;texts.forEach(({ config, name }) => {const textNode = stageNode.findOne(`.${name}`);if (textNode && !config.value.dragged) {// 直接使用预设的stageSize.width,避免依赖实时计算const textWidth = textNode.width();config.value.x = stageSize.width / 2 - textWidth / 2;}});stageNode.draw(); // 强制重绘});
};let drawController = new DrawPosterImage();const createPoster = async (backgroundImg: string, preserveColors = false) => {const bgImg = backgroundImg.length > 0 ? backgroundImg : unref(prevBgImg);const avatar = unref(templateConf)?.posterAvatar?.length? unref(templateConf)?.posterAvatar: defaultAvatar;await Promise.all([await drawController.drawBackground(bgImg, preserveColors),(await showCustomAvatar.value)? drawController.drawCreaterAvatar(avatar): drawController.drawCreaterAvatar(),drawController.drawCreaterNickName(unref(templateConf)?.posterNickname ?? unref(defaultNickName),),await drawController.drawQRCode(unref(qrcodeUrl)),await drawController.drawLogoImg(unref(showCustomLogo) ? unref(leftLogo) : ''),await drawController.drawFormTitle(unref(templateConf).name || '未命名表单'),await drawController.drawInvitation(unref(shareInputModel)),]).then(() => {drawController.getDataUrl();prevBgImg.value = bgImg;// updateData();});
};const bgImgList = shareBgConf.shareBgList; // 会有模板这里模板的背景list// 因为要记录下当前的修改,所以这部分存在了pinia当中,刷新页面后回归原样。
onMounted(async () => {const setInitialTextPositions = () => {// 标题const formTitle = unref(templateConf).name || '未命名表单';const formTitleWidth = calculateTextWidth(formTitle, 16, 'bold');formTitleConfig.value.x = (stageSize.width - formTitleWidth) / 2;// 昵称const nickName = unref(nickNameInputModel) || unref(templateConf)?.posterNickname || '新用户';const nickNameWidth = calculateTextWidth(nickName, 16);nickNameConfig.value.x = (stageSize.width - nickNameWidth) / 2;// 邀请语const inviteText = unref(shareInputModel) || '邀请您填写';const inviteWidth = calculateTextWidth(inviteText, 14);inviteConfig.value.x = (stageSize.width - inviteWidth) / 2;// 邀请tipsconst tipsText = '长按扫码进行填写';const tipsWidth = calculateTextWidth(tipsText, 14);tipsConfig.value.x = (stageSize.width - tipsWidth) / 2;};setInitialTextPositions(); // 初始设置正确位置if (unref(shareData)?.shareCustomAvatar) {avatarImg.value = unref(shareData)?.shareCustomAvatar || defaultAvatar;} else {if ((unref(templateConf)?.posterAvatar || '').length > 0) {avatarImg.value = (await getSafeImg(unref(templateConf)!.posterAvatar!)) || '';} else {avatarImg.value = defaultAvatar;}}await nextTick();await createPoster(unref(customBg) || bgImgList[0], unref(customBg) ? false : true);updateTextPositions();}
});// 移动事件实时更新数据,因为我要保存下当前的位置等信息
const handleDragMove = (e) => {const node = e.target;const { width: stageWidth, height: stageHeight } = stageSize;// 直接忽略不需要处理的节点if (node.getName() === 'image') {handleBackgroundDrag(node);return;}// 通用边界限制函数const constrainPosition = (value, min, max) => Math.max(min, Math.min(value, max));// 计算节点的边界限制let newX = node.x();let newY = node.y();if (node.getName() === 'circle') {const radius = node.radius();newX = constrainPosition(newX, radius / 2, stageWidth - radius / 2);newY = constrainPosition(newY, radius / 2, stageHeight - radius / 2);} else {const width = node.width ? node.width() : 0;const height = node.height ? node.height() : 0;newX = constrainPosition(newX, 0, stageWidth - width);newY = constrainPosition(newY, 0, stageHeight - height);}// 更新节点位置node.x(newX);node.y(newY);const configMap = {inviteText: inviteConfig,nickNameText: nickNameConfig,formTitleText: formTitleConfig,tipsText: tipsConfig,qrGroup: qrGroupConfig,logo: logoConfig,circle: avatarConfig,};const config = configMap[node.getName()];if (config) {config.value.x = newX;config.value.y = newY;config.value.dragged = true;updateData();}
};const updateData = () => {const elementPositions = {formTitle: {x: formTitleConfig.value.x,y: formTitleConfig.value.y,fontSize: formTitleConfig.value.fontSize,fill: formTitleConfig.value.fill,fontStyle: formTitleConfig.value.fontStyle,},invite: {x: inviteConfig.value.x,y: inviteConfig.value.y,fontSize: inviteConfig.value.fontSize,fill: inviteConfig.value.fill,fontStyle: inviteConfig.value.fontStyle,},nickName: {x: nickNameConfig.value.x,y: nickNameConfig.value.y,fontSize: nickNameConfig.value.fontSize,fill: nickNameConfig.value.fill,fontStyle: nickNameConfig.value.fontStyle,},tips: {x: tipsConfig.value.x,y: tipsConfig.value.y,fontSize: tipsConfig.value.fontSize,fill: tipsConfig.value.fill,fontStyle: tipsConfig.value.fontStyle,},qrGroup: {x: qrGroupConfig.value.x,y: qrGroupConfig.value.y,},logo: {x: logoConfig.value.x,y: logoConfig.value.y,},avatar: {x: avatarConfig.value.x,y: avatarConfig.value.y,},};emit('update', {formKey: unref(formkey),shareTitle: unref(shareInputModel),shareNickName: unref(nickNameInputModel),shareCustomBackGround: unref(customBg).length > 0 ? unref(prevBgImg) : '',// shareCustomLogo: unref(logoImg),// shareCustomAvatar: unref(avatarImg),qrcodeType: unref(qrcodeType),showCustomLogo: unref(showCustomLogo),showCustomAvatar: unref(showCustomAvatar),elementPositions,});
};

以上是我的ts部分,封装了个类里边是相关绘制方法以及各自的配置。
至于右侧的设置,就是当修改后画布重新绘制即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/88425.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/88425.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

政安晨【开源人工智能硬件】【ESP乐鑫篇】 —— 在macOS上部署工具开发环境(小资的非开发者用苹果系统也可以玩乐鑫)

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; 前言 开源人工智能硬件会给你带来无限可能&#xff0c;玩开源硬件&#xff0c;环境和工具少…

Vue3 学习教程,从入门到精通,vue3学习中的JavaScript ES6 特性详解与案例(5)

vue3学习中的JavaScript ES6 特性详解与案例 ES6&#xff08;ECMAScript 2015&#xff09;是 JavaScript 的一个重要版本&#xff0c;引入了许多新特性&#xff0c;极大地提升了语言的表达能力和开发效率。本文将详细介绍 ES6 的主要特性&#xff0c;包括 let 和 const 命令、变…

深度学习模型1:理解LSTM和BiLSTM

深度学习模型1&#xff1a;理解LSTM和BiLSTM 因为最近科研复现论文中需要了解单向LSTM和双向LSTM&#xff0c;所以就学习了一下LSTM的基本原理&#xff0c;下面孬孬带着大家一起学习一下&#xff0c;感谢大家的一键三连 一、RNN 因为谈到LSTM&#xff0c;就必不可少的会考虑RNN…

[论文阅读] 软件工程 | 一篇关于开源许可证管理的深度综述

关于开源许可证管理的深度综述 论文标题&#xff1a;Open Source, Hidden Costs: A Systematic Literature Review on OSS License ManagementarXiv:2507.05270 Open Source, Hidden Costs: A Systematic Literature Review on OSS License Management Boyuan Li, Chengwei Liu…

Qt悬浮动态

粉丝悬浮动态&#xff0c;及抽奖程序#include "masklabel.h"MaskLabel::MaskLabel(int pos_x,QString fans_name,QWidget*parent):QLabel(parent) {this->setAlignment(Qt::AlignHCenter);//设置字体居中this->setStyleSheet("color:white;font-size:20px…

深入拆解Spring思想:DI(依赖注入)

在简单了解IoC与DI中我们已经了解了DI的基本操作&#xff0c;接下来我们来详解DI。(IoC详解请看这里)我们已经知道DI是“你给我&#xff0c;我不用自己创建”的原则。现在我们来看看Spring是如何实现“给”这个动作的&#xff0c;也就是依赖注入的几种方式。 Spring主要提供了…

Arcgis连接HGDB报错

文章目录环境症状问题原因解决方案环境 系统平台&#xff1a;Linux x86-64 Red Hat Enterprise Linux 7 版本&#xff1a;6.0 症状 Arcgis连接HGDB报错&#xff1a; 无法连接到数据库服务器来检索数据库列表&#xff1b;请检查服务器名称、用户名和密码信息&#xff0c;然后…

Android 应用常见安全问题

背景&#xff1a;OWASP MASVS(Mobile Application Security Verification Standard 移动应用安全验证标准&#xff09;是移动应用安全的行业标准。 一、MASVS-STORAGE&#xff1a;存储 1.1 不当暴露FileProvider目录 配置不当的 FileProvider 会无意中将文件和目录暴露给攻击者…

Netty的内存池机制怎样设计的?

大家好&#xff0c;我是锋哥。今天分享关于【Netty的内存池机制怎样设计的?】面试题。希望对大家有帮助&#xff1b; Netty的内存池机制怎样设计的? 超硬核AI学习资料&#xff0c;现在永久免费了&#xff01; Netty的内存池机制是为了提高高并发环境下的内存分配与回收效率…

Python 项目快速部署到 Linux 服务器基础教程

Linux的开源特性和强大的命令行工具使得部署流程高度自动化&#xff0c;可重复性强。本文将详细介绍如何从零开始快速部署Python项目到Linux服务器。 Linux系统因其稳定性、安全性和性能优化&#xff0c;成为Python项目部署的首选平台。无论是使用flask构建Web应用、FastAPI创…

SQL Server通过CLR连接InfluxDB实现异构数据关联查询技术指南

一、背景与需求场景 在工业物联网和金融监控场景中,实时时序数据(InfluxDB)需与业务元数据(SQL Server)联合分析: 工业场景:设备传感器每秒采集温度、振动数据(InfluxDB),需关联工单状态、设备型号(SQL Server)金融场景:交易流水时序数据(每秒万条)需实时匹配客…

机器学习详解

## 深入解析机器学习&#xff1a;核心概念、方法与未来趋势机器学习&#xff08;Machine Learning, ML&#xff09;作为人工智能的核心分支&#xff0c;正深刻重塑着我们的世界。本文将系统介绍机器学习的基本概念、主要方法、实际应用及未来挑战&#xff0c;为您提供全面的技术…

汽车间接式网络管理的概念

在汽车网络管理中&#xff0c;直接式和间接式管理是两种用于协调车载电子控制单元&#xff08;ECUs&#xff09;之间通信与行为的机制。它们主要用于实现车辆内部不同节点之间的协同工作&#xff0c;特别是在涉及网络唤醒、休眠、状态同步等场景中。### 直接式管理直接式网络管…

npm : 无法加载文件 D:\Node\npm.ps1,因为在此系统上禁止运行脚本。

npm : 无法加载文件 D:\Node\npm.ps1&#xff0c;因为在此系统上禁止运行脚本。 安装高版本的node.js&#xff0c;可能会导致这个问题&#xff0c; 脚本的权限被限制了&#xff0c;需要你设置用户权限。 get-ExecutionPolicy set-ExecutionPolicy -Scope CurrentUser remotesig…

搜索算法讲解

搜索算法讲解 深度优先搜索-DFS P1219 [USACO1.5] 八皇后 Checker Challenge 一个如下的 666 \times 666 的跳棋棋盘&#xff0c;有六个棋子被放置在棋盘上&#xff0c;使得每行、每列有且只有一个&#xff0c;每条对角线&#xff08;包括两条主对角线的所有平行线&#xff…

深度学习---Rnn-文本分类

# 导入PyTorch核心库 import torch # 导入神经网络模块 import torch.nn as nn # 导入优化器模块 import torch.optim as optim # 导入函数式API模块 import torch.nn.functional as F # 导入数据集和数据加载器 from torch.utils.data import Dataset, DataLoader # 导入NumPy…

20250709解决KickPi的K7开发板rk3576-android14.0-20250217.tar.gz编译之后刷机启动不了

【整体替换】 Z:\20250704\rk3576-android14.0\rkbin清理编译的临时结果&#xff1a; rootrootrootroot-X99-Turbo:~$ cd 14TB/versions/rk3576-android14.0-20250217k7/ rootrootrootroot-X99-Turbo:~/14TB/versions/rk3576-android14.0-20250217k7$ ll rootrootrootroot-X99-…

怎么创建新的vue项目

首先&#xff0c;新建一个文件点文件路径&#xff0c;输入cmd

CIU32L051系列 DMA串口无阻塞性收发的实现

1.CIU32L051 DMA的通道映射由于华大CIU32L051的DMA外设资源有限&#xff0c;DMA只有两个通道可供使用&#xff0c;对应的通道映射图如下&#xff1a;2.UART对应的引脚分布及其复用映射CIU32L051对应的UART对应的引脚映射图如下,这里博主为了各位方便查找&#xff0c;就直接全拿…

飞算 JavaAI 体验:重塑 Java 开发的智能新范式

飞算 JavaAI 体验&#xff1a;重塑 Java 开发的智能新范式引言&#xff1a;正文&#xff1a;一、工程化代码生成&#xff1a;从 "片段拼接" 到 "模块交付"1.1 传统工具的局限与突破1.2 代码质量验证二、智能重构引擎&#xff1a;从 "问题修复" 到…