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
英文地址,請注明出處:http://en.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;從 "問題修復" 到…