uniapp使用Canvas给图片加水印把临时文件上传到服务器

生成的临时路径是没有完整的路径没办法上传到服务器
16:37:40.993 添加水印后的路径, _doc/uniapp_temp_1710923708347/canvas/17109238597881.png
16:37:41.041 添加水印后的完整路径, file://storage/emulated/0/Android/data/com.jingruan.zjd/apps/__UNI__BE4B000/doc/uniapp_temp_1710923708347/canvas/17109238597881.png
使用以下代码得到完整的路径
let path = 'file:/' + plus.io.convertLocalFileSystemURL(tempFilePath);

完整代码如下 使用的插件市场的hpy-watermark组件 一共2个

效果是

其他页面调用方式
<!--
增加水印上传villageReviewForm.preciseAddr 是通过高德获取的定位地址
-->
<hpy-watermark ref="uploadImage" :address="villageReviewForm.preciseAddr" @waterMark="waterMark"></hpy-watermark>
高德获取定位
uni.getLocation({type: 'gcj02',geocode: true,isHighAccuracy: true,success: res => { const {province,city,district,street,streetNum,poiName} = res.address;this.villageReviewForm.preciseAddr =`${district}${street}${streetNum}${poiName}${res.longitude},${res.latitude}`;console.log("经纬度地点",this.villageReviewForm.preciseAddr)// 数据渲染this.$forceUpdate();}});
获取上传数据结果
const fileList = this.$refs.uploadImage.fileList
组件样式

hpy-watermark.vue
<template><view><view class="watermark-content"><canvas canvas-id="watermarkCanvas" id="watermarkCanvas" :style="{width:canvasWidth + 'px', height:canvasHeight + 'px'}"></canvas></view><upload-image v-model="fileList" style="margin-left: 15rpx" :image-styles="imageStyles" :files-list="filesList" :delIcon="delIcon" @choose="chooseImage" @delFile="delFile"><slot><view class="is-add"><view class="icon-add"></view><view class="icon-add rotate"></view></view></slot></upload-image></view>
</template><script>import {fileServerIp} from "@/common/utils/config.js"import Session from "@/common/Session";import uploadImage from './upload-image.vue'export default {components: {uploadImage, },name:'hpy-watermark',props:{address:{type:String,default:''},delIcon: {type: Boolean,default: true},listStyles: {type: Object,default () {return {// 是否显示边框border: true,// 是否显示分隔线dividline: true,// 线条样式borderStyle: {}}}},imageStyles: {type: Object,default () {return {width: 'auto',height: 'auto'}}},/*** 文字文字位置(默认:左下角)可选值:左上角:topLeft、右上角:topRight、左下角:bottomLeft、右下角:bottomRight*/markAlign:{type:String,default:function(){return 'bottomLeft'}},/*** 设置文本的水平对齐方式,默认:start,文本在指定的位置开始。* end 文本在指定的位置结束。* center 文本的中心被放置在指定的位置。* left 文本左对齐。* right 文本右对齐。*/textAlign:{type:String,default:function(){return 'start';}},/*** 设置文本的垂直对齐方式,默认:alphabetic文本基线是普通的字母基线。* top 文本基线是 em 方框的顶端。* hanging 文本基线是悬挂基线。* middle 文本基线是 em 方框的正中。* ideographic 文本基线是表意基线。* bottom 文本基线是 em 方框的底端。*/textBaseline:{type:String,default:function(){return 'alphabetic';}},/*** 文字大小*/fontSize:{type:[Number, String],default:30},/*** 文字颜色*/fontColor:{type:String,default:function(){return 'red'}},/*** 阴影颜色*/shadowColor:{type:String,default:function(){return 'rgba(0, 0, 0, 1.0)';}},/*** 阴影边框大小*/shadowWidth:{type:[Number, String],default:2},/*** 图片的质量,取值范围为 (0, 1],不在范围内时当作1处理*/quality:{type:[Number, String],default:1},/*** 目标文件的类型,只支持 'jpg' 或 'png'。默认为 'png'*/fileType:{type:String,default:function(){return 'png'}}},data() {return {fileList: [],files: [],filesList:[],canvasWidth:0,canvasHeight:0};},watch: { fileList: {handler(newVal, oldVal) { this.filesList=newVal;},immediate: true},},methods: {// 选择图片chooseImage() {if(this.isEmpty(this.address)){uni.showToast({icon:'none',title:'请打开定位或者重新获取'});return;}uni.chooseImage({count: this.limit, // 限制的图片数量sizeType: ['compressed'], // original 原图,compressed 压缩图,默认二者都有 sourceType: [ 'camera'],// album 从相册选图,camera 使用相机,默认二者都有success: (res) => {var imgPathList = res.tempFilePaths;if(imgPathList.length > 0){this.addImages(imgPathList);}},fail: (err) => {console.log('chooseImage fail', err)if("chooseImage:fail cancel" == err.errMsg){uni.showToast({icon:'none',title:'取消了选择'});}else{}}});},// 添加图片addImages(filePaths){if(filePaths.length > 0){var fillTexts = ["地址:"+this.address];fillTexts.push("时间:" + this.getNowTime());// 添加水印this.addWaterMark({filePaths,fillTexts});}},/*** 水印添加回调,在H5平台下,filePath 为 base64*/waterMark(filePath){this.imageList.push(filePath);},/*** 获取当前时间*/getNowTime(){var date = new Date(),year = date.getFullYear(),month = date.getMonth() + 1,day = date.getDate(),hour = date.getHours() < 10 ? "0" + date.getHours() : date.getHours(),minute = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes(),second = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();month >= 1 && month <= 9 ? (month = "0" + month) : "";day >= 0 && day <= 9 ? (day = "0" + day) : "";return (year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);},/*** 删除文件* @param {Object} index*/delFile(index) {this.$emit('delete', {tempFile: this.filesList[index],tempFilePath: this.filesList[index].url})this.filesList.splice(index, 1) },/*** 增加水印* @param {Object} {filePaths:['图片地址1', '图片地址2'], fillTexts:['水印1', '水印2']}*/async addWaterMark({ filePaths = [], fillTexts = [] }) {try{for (const filePath of filePaths) {await this.drawImage(filePath, fillTexts.reverse());}}catch(e){// TODO handle the exception}finally{// uni.hideLoading();}},/*** 绘制单个图片*/async drawImage(filePath, fillTexts){uni.showLoading({title:'图片处理中···'});const ctx = uni.createCanvasContext('watermarkCanvas', this);return new Promise(resolve => {uni.getImageInfo({src: filePath,success: (image) => {this.canvasWidth = image.width;this.canvasHeight = image.height;ctx.clearRect(0, 0, image.width, image.height);setTimeout(()=>{ctx.drawImage(image.path, 0, 0, image.width, image.height);ctx.setFontSize(this.fontSize);ctx.setFillStyle(this.fontColor);// 设置阴影let shadowWidth = Number(this.shadowWidth + "");if(shadowWidth > 0){ctx.shadowColor = this.shadowColor;ctx.shadowOffsetX = shadowWidth;ctx.shadowOffsetY = shadowWidth;}// 设置水平对齐方式ctx.textAlign = this.textAlign;// 设置垂直对齐方式ctx.textBaseline = this.textBaseline;const maxText = fillTexts.reduce((text, val) => {return text.length >= val.length ? text : val;});fillTexts.forEach((mark, index) => {if(this.markAlign == "bottomRight"){ctx.fillText(mark, image.width - (ctx.measureText(maxText).width+60), image.height - (index*60+60));}else if(this.markAlign == "topLeft"){ctx.fillText(mark, 20, (index*60+60));}else if(this.markAlign == "topRight"){ctx.fillText(mark, image.width - (ctx.measureText(maxText).width+60), (index*60+60));}else{ctx.fillText(mark, 20, image.height - (index*60+60));}});ctx.draw(false, (() => {setTimeout(()=>{uni.canvasToTempFilePath({canvasId: 'watermarkCanvas',fileType:this.fileType,quality:Number(this.quality + "" || "1"),success: (res) => { console.log("添加水印后的路径",res.tempFilePath )this.saveUploadImage(res.tempFilePath )},fail:(err) => {uni.hideLoading();console.log(err)},complete: () => {resolve();}}, this);}, 300);})());}, 200);},fail: (e) => {resolve();}});});},saveUploadImage(tempFilePath){uni.showLoading({title:'图片上传中···'});// #ifdef APP-PLUSvar p = plus.io.convertLocalFileSystemURL(tempFilePath);this.url = 'file:/' + pconsole.log("添加水印后的完整路径",this.url )// #endifuni.uploadFile({url: fileServerIp + 'common/upload',name: "file",// #ifdef H5filePath: tempFilePath,// #endif// #ifdef APP-PLUSfilePath: this.url,// #endifheader: {Authorization: "Bearer " + Session.getValue('token')},success: uploadFileRes => {uni.hideLoading();const {data} = JSON.parse(uploadFileRes.data)this.filesList.push({url: data.url,name: data.fileName,extname: 'png'}) this.$emit('waterMark',{url: data.url,name: data.fileName,extname: 'png'});},fail: error => {uni.hideLoading();uni.showToast({title: '上传失败!',icon: 'error',duration: 2000});}})}}}
</script><style scoped>.watermark-content{width: 0;height: 0;overflow: hidden;}.uni-file-picker__container {/* #ifndef APP-NVUE */display: flex;box-sizing: border-box;/* #endif */flex-wrap: wrap;margin: -5px;}.rotate {position: absolute;transform: rotate(90deg);}.icon-add {width: 50px;height: 5px;background-color: #f1f1f1;border-radius: 2px;}
</style>
upload-image.vue
<template><view class="uni-file-picker__container"><view class="file-picker__box" v-for="(item,index) in filesList" :key="index" :style="boxStyle"><view class="file-picker__box-content" :style="borderStyle"><image class="file-image" :src="item.url" mode="aspectFill" @click.stop="prviewImage(item,index)"></image><view v-if="delIcon && !readonly" class="icon-del-box" @click.stop="delFile(index)"><view class="icon-del"></view><view class="icon-del rotate"></view></view><view v-if="(item.progress && item.progress !== 100) ||item.progress===0 " class="file-picker__progress"><progress class="file-picker__progress-item" :percent="item.progress === -1?0:item.progress" stroke-width="4":backgroundColor="item.errMsg?'#ff5a5f':'#EBEBEB'" /></view><view v-if="item.errMsg" class="file-picker__mask" @click.stop="uploadFiles(item,index)">点击重试</view></view></view><view v-if="filesList.length < limit && !readonly" class="file-picker__box" :style="boxStyle"><view class="file-picker__box-content is-add" :style="borderStyle" @click="choose"><slot><view class="icon-add"></view><view class="icon-add rotate"></view></slot></view></view></view>
</template><script>export default {name: "uploadImage",emits:['uploadFiles','choose','delFile'],props: {filesList: {type: Array,default () {return []}},disabled:{type: Boolean,default: false},disablePreview: {type: Boolean,default: false},limit: {type: [Number, String],default: 9},imageStyles: {type: Object,default () {return {width: 'auto',height: 'auto',border: {}}}},delIcon: {type: Boolean,default: true},readonly:{type:Boolean,default:false}},computed: {styles() {let styles = {width: 'auto',height: 'auto',border: {}}return Object.assign(styles, this.imageStyles)},boxStyle() {const {width = 'auto',height = 'auto'} = this.styleslet obj = {}if (height === 'auto') {if (width !== 'auto') {obj.height = this.value2px(width)obj['padding-top'] = 0} else {obj.height = 0}} else {obj.height = this.value2px(height)obj['padding-top'] = 0}if (width === 'auto') {if (height !== 'auto') {obj.width = this.value2px(height)} else {obj.width = '33.3%'}} else {obj.width = this.value2px(width)}let classles = ''for(let i in obj){classles+= `${i}:${obj[i]};`}return classles},borderStyle() {let {border} = this.styleslet obj = {}const widthDefaultValue = 1const radiusDefaultValue = 3if (typeof border === 'boolean') {obj.border = border ? '1px #eee solid' : 'none'} else {let width = (border && border.width) || widthDefaultValuewidth = this.value2px(width)let radius = (border && border.radius) || radiusDefaultValueradius = this.value2px(radius)obj = {'border-width': width,'border-style': (border && border.style) || 'solid','border-color': (border && border.color) || '#eee','border-radius': radius}}let classles = ''for(let i in obj){classles+= `${i}:${obj[i]};`}return classles}},methods: {uploadFiles(item, index) {this.$emit("uploadFiles", item)},choose() {this.$emit("choose")},delFile(index) {this.$emit('delFile', index)},prviewImage(img, index) {let urls = []if(Number(this.limit) === 1&&this.disablePreview&&!this.disabled){this.$emit("choose")}if(this.disablePreview) returnthis.filesList.forEach(i => {urls.push(i.url)})uni.previewImage({urls: urls,current: index});},value2px(value) {if (typeof value === 'number') {value += 'px'} else {if (value.indexOf('%') === -1) {value = value.indexOf('px') !== -1 ? value : value + 'px'}}return value}}}
</script><style lang="scss">.uni-file-picker__container {/* #ifndef APP-NVUE */display: flex;box-sizing: border-box;/* #endif */flex-wrap: wrap;margin: -5px;}.file-picker__box {position: relative;// flex: 0 0 33.3%;width: 33.3%;height: 0;padding-top: 33.33%;/* #ifndef APP-NVUE */box-sizing: border-box;/* #endif */}.file-picker__box-content {position: absolute;top: 0;right: 0;bottom: 0;left: 0;margin: 5px;border: 1px #eee solid;border-radius: 5px;overflow: hidden;}.file-picker__progress {position: absolute;bottom: 0;left: 0;right: 0;/* border: 1px red solid; */z-index: 2;}.file-picker__progress-item {width: 100%;}.file-picker__mask {/* #ifndef APP-NVUE */display: flex;/* #endif */justify-content: center;align-items: center;position: absolute;right: 0;top: 0;bottom: 0;left: 0;color: #fff;font-size: 12px;background-color: rgba(0, 0, 0, 0.4);}.file-image {width: 100%;height: 100%;}.is-add {/* #ifndef APP-NVUE */display: flex;/* #endif */align-items: center;justify-content: center;}.icon-add {width: 50px;height: 5px;background-color: #f1f1f1;border-radius: 2px;}.rotate {position: absolute;transform: rotate(90deg);}.icon-del-box {/* #ifndef APP-NVUE */display: flex;/* #endif */align-items: center;justify-content: center;position: absolute;top: 3px;right: 3px;height: 26px;width: 26px;border-radius: 50%;background-color: rgba(0, 0, 0, 0.5);z-index: 2;transform: rotate(-45deg);}.icon-del {width: 15px;height: 2px;background-color: #fff;border-radius: 2px;}
</style>
相关文章:
uniapp使用Canvas给图片加水印把临时文件上传到服务器
生成的临时路径是没有完整的路径没办法上传到服务器 16:37:40.993 添加水印后的路径, _doc/uniapp_temp_1710923708347/canvas/17109238597881.png 16:37:41.041 添加水印后的完整路径, file://storage/emulated/0/Android/data/com.jingruan.zjd/apps/__UNI__BE4B000/doc/…...
小希的迷宫
目录 描述 输入 输出 样例输入 样例~~输出~~ 思路 code 描述 Gardon的迷宫城堡小希玩了很久(见Problem B),现在她也想设计一个迷宫让Gardon来走。但是她设计迷宫的思路不一样,首先她认为所有的通道都应该是双向连通的&…...
MySQL索引剖析【了解背后的数据结构】
文章目录 常用索引概念聚簇索引 🎉非聚簇索引(二级索引) 数据结构选择Hash结构 ⭐️有序数组二叉搜索树AVL树(平衡二叉搜索树)B-Tree(多路平衡查找树)BTree ⭐️ MySQL中索引的实现InnoDB 索引实…...
004——内存映射(基于鸿蒙和I.MAX6ULL)
目录 一、 ARM架构内存映射模型 1.1 页表项 1.2 一级页表映射过程 1.3 二级页表映射过程 1.4 cache 和 buffer 二、 鸿蒙内存映射代码学习 三、 为板子编写内存映射代码 3.1 内存地址范围 3.2 设备地址范围 一、 ARM架构内存映射模型 (以前我以为页表机制…...
150 Linux C++ 通讯架构实战6 服务器程序目录规划,makefile编写
从无到有产生这套 通讯架构源代码【项目/工程】 一,服务器程序目录规划 一个完整的项目 肯定会有多个源文件,头文件,会分别存放到多个目录; 我们这里要规划项目的目录结构; 注意:不固安是目录还是文件&am…...
OpenCV支持哪些类型的文件格式读写?
OpenCV支持多种类型的文件格式读写,包括但不限于以下格式: Windows位图文件:包括BMP和DIB格式。JPEG文件:支持JPEG、JPG和JPE三种扩展名。便携式网络图片:即PNG格式。便携式图像格式:包括PBM、PGM和PPM三种…...
数据库中使用IN操作效率问题
1. IN操作的基本概念 IN操作符在SQL中用于指定某个字段的值是否匹配列表中的任何值。这是一个条件操作符,用于在WHERE子句中过滤记录。 SQL语法示例: SELECT * FROM table_name WHERE column_name IN (value1, value2, ...); 2. IN操作的效率问题 当…...
unity学习(67)——控制器Joystick Pack方向
1.轮盘直接复制一个拖到右边就ok了,轮盘上是有脚本的。(只复制) 2.上面的显示窗也可以复制,但是要绑定对应的轮盘(unity中修改变量),显示窗上是有脚本的。(复制改变量) 3…...
MATLAB的使用(一)
一,MATLAB的编程特点 a,语法高度简化; b,脚本式解释型语言; c,针对矩阵的高性能运算; d,丰富的函数工具箱支持; e,通过matlab本体构建跨平台; 二,MATLAB的界面 工具栏:提供快捷操作编辑器…...
JMeter并发工具的使用
视频地址:Jmeter安装教程01_Jmeter之安装以及环境变量配置_哔哩哔哩_bilibili 一、JMeter是什么 JMeter是一款免安装包,官网下载好后直接解压缩并配置好环境变量就可以使用。 环境变量配置可参考:https://www.cnblogs.com/liulinghua90/p/…...
基于springboot+vue的毕业就业信息管理系统
博主主页:猫头鹰源码 博主简介:Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战,欢迎高校老师\讲师\同行交流合作 主要内容:毕业设计(Javaweb项目|小程序|Pyt…...
有什么小程序适合个人开发?
在这个信息爆炸的时代,小程序已经成为了我们生活中的一部分。无论是出行、购物还是娱乐,小程序都能为我们提供便捷的服务。对于个人开发者来说,开发一个小程序不仅可以锻炼自己的技术能力,还可以为他人提供便利,甚至有…...
【ARXIV2402】MambaIR
这个工作首次将 Mamba 引入到图像修复任务,关于为什么 Mamba 可以用于图像修复,作者有非常详细的解释:一路向北:性能超越SwinIR!MambaIR: 基于Mamba的图像复原基准模型 作者认为Mamba可以理解为RNN和CNN的结合…...
【计算机网络篇】数据链路层(3)差错检测
文章目录 🥚误码🍔两种常见的检错技术⭐奇偶校验⭐循环冗余校验🎈例子 🥚误码 误码首先介绍误码的相关概念 🍔两种常见的检错技术 ⭐奇偶校验 奇校验是在待发送的数据后面添加1个校验位,使得添加该校验…...
软件配置管理计划
1. 配置管理目标 本软件配置管理计划的目标在于确保软件开发生命周期内的所有配置项(CI)都得到适当的标识、控制、版本管理和追踪。通过实施有效的配置管理,我们的目标是: 保持配置项的一致性和完整性。确保配置项的可追溯性。减…...
嵌入式备考错题汇总
若某条无条件转移汇编指令采用直接寻址,则该指令的功能是将指令中的地址码送入()。 A.PC(程序计数器) B.AR(地址寄存器) C.AC(累加器) D.ALU(算术逻辑运算单元) 解析:选A,直接寻址是指操作数存放在内存单元中,指令中直接给出操作数所在存储单…...
38 mars3d 对接地图图层 绘制点线面员
前言 这里主要是展示一下 mars3d 的一个基础的使用 主要是设计 接入地图服务器的 卫星地图, 普通的二维地图, 增加地区标记 基础绘制 点线面园 等等 测试用例 <template><div style"width: 1920px; height:1080px;"><div class"mars3dClas…...
什么是Webhook 和 HTTP Endpoint?
Webhook 和 HTTP Endpoint 都是基于HTTP协议的网络通信概念,但它们在使用场景和目的上有所不同。 Webhook Webhook 是一种允许一个应用程序提供实时信息给其他应用程序的方法,这种通信是基于HTTP的“回调”或“钩子”。Webhook 通常被用来在一种服务上…...
小程序跨端组件库 Mpx-cube-ui 开源:助力高效业务开发与主题定制
Mpx-cube-ui 是一款基于 Mpx 小程序框架的移动端基础组件库,一份源码可以跨端输出所有小程序平台及 Web,同时具备良好的拓展能力和可定制化的能力来帮助你快速构建 Mpx 应用项目。 Mpx-cube-ui 提供了灵活配置的主题定制能力,在组件设计开发阶…...
GDC期间LayaAir启动全球化战略
3 月 18 日至 3 月 22 日,一年一度的游戏开发者大会(GDC)在美国旧金山举行。在此期间,Layabox宣布LayaAir引擎启动全球扩张战略,这标志着引擎将步入快速发展的新阶段。此举旨在利用公司先进的3D引擎技术,将…...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...
装饰模式(Decorator Pattern)重构java邮件发奖系统实战
前言 现在我们有个如下的需求,设计一个邮件发奖的小系统, 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其…...
RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
MySQL用户和授权
开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务: test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...
书籍“之“字形打印矩阵(8)0609
题目 给定一个矩阵matrix,按照"之"字形的方式打印这个矩阵,例如: 1 2 3 4 5 6 7 8 9 10 11 12 ”之“字形打印的结果为:1,…...
aardio 自动识别验证码输入
技术尝试 上周在发学习日志时有网友提议“在网页上识别验证码”,于是尝试整合图像识别与网页自动化技术,完成了这套模拟登录流程。核心思路是:截图验证码→OCR识别→自动填充表单→提交并验证结果。 代码在这里 import soImage; import we…...
高分辨率图像合成归一化流扩展
大家读完觉得有帮助记得关注和点赞!!! 1 摘要 我们提出了STARFlow,一种基于归一化流的可扩展生成模型,它在高分辨率图像合成方面取得了强大的性能。STARFlow的主要构建块是Transformer自回归流(TARFlow&am…...
Qt Quick Controls模块功能及架构
Qt Quick Controls是Qt Quick的一个附加模块,提供了一套用于构建完整用户界面的UI控件。在Qt 6.0中,这个模块经历了重大重构和改进。 一、主要功能和特点 1. 架构重构 完全重写了底层架构,与Qt Quick更紧密集成 移除了对Qt Widgets的依赖&…...
【threejs】每天一个小案例讲解:创建基本的3D场景
代码仓 GitHub - TiffanyHoo/three_practices: Learning three.js together! 可自行clone,无需安装依赖,直接liver-server运行/直接打开chapter01中的html文件 运行效果图 知识要点 核心三要素 场景(Scene) 使用 THREE.Scene(…...
