- 小程序背景音乐
js: const bgMusic = wx.getBackgroundAudioManager();//背景音频 Page({ /** * 页面的初始数据 */ data: { musicArr:[], // 音乐播放器 bgAudio:{ index:0, isOpen: false,//播放开关 starttime: '00:00', //正在播放时长 duration: '00:00', //总时长 src:"", title:"", }, }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { let that=this; that.data.musicArr=[{ id:1, name:"红色基因绿色源泉", src:"/store/activity/audio/mpeg/41FFCFBD5494DFE67FE7E22AC54EEB282021092926031664", duration:"04:20" },{ id:2, name:"我和我的祖国", src:"/store/activity/audio/mpeg/D342C12632FC462D142FEA8A80677D872021092941031658", duration:"03:08" }]; that.setData({ musicArr:that.data.musicArr, }) that.getMusic(0); }, getMusic(index){ this.data.bgAudio.index=index; this.data.bgAudio.duration=this.data.musicArr[index].duration; this.data.bgAudio.src=this.data.musicArr[index].src; this.data.bgAudio.title=this.data.musicArr[index].name; this.setData({ bgAudio:this.data.bgAudio }) }, // 播放 ButtonPlay: function () { // wx.showLoading({ // title: '加载中...', // }) var that = this; //bug ios 播放时必须加title 不然会报错导致音乐不播放 bgMusic.title = that.data.bgAudio.title; bgMusic.epname = that.data.bgAudio.title; bgMusic.src = that.data.bgAudio.src; that.data.bgAudio.isOpen=true; bgMusic.onTimeUpdate(() => { //bgMusic.duration总时长 bgMusic.currentTime当前进度 var duration = bgMusic.duration; var offset = bgMusic.currentTime; var currentTime = parseInt(offset); var min = "0" + parseInt(currentTime / 60); var max = parseInt(duration); var sec = currentTime % 60; if (sec < 10) { sec = "0" + sec; }; var starttime = min + ':' + sec; /* 00:00 */ that.data.bgAudio.offset=currentTime; that.data.bgAudio.starttime=starttime; that.data.bgAudio.max=max; that.setData({ bgAudio:that.data.bgAudio }) console.log("index.js",starttime) }) //播放结束 bgMusic.onEnded(() => { that.data.bgAudio.starttime='00:00'; that.data.bgAudio.isOpen=false; that.data.bgAudio.offset=0; that.setData({ bgAudio:that.data.bgAudio }) bgMusic.stop(); // 循环播放 播放完最后一条循环播放 if(that.data.bgAudio.index { console.log("播放中index.js") that.getHits(that.data.musicArr[that.data.bgAudio.index].id); that.data.bgAudio.isOpen=true; that.setData({ bgAudio:that.data.bgAudio }) }) }, //暂停播放 ButtonPause(){ var that = this bgMusic.pause(); that.data.bgAudio.isOpen=false; that.setData({ bgAudio:that.data.bgAudio }) }, // 进度条拖拽 sliderChangeBj(e) { var that = this var offset = parseInt(e.detail.value); bgMusic.play(); bgMusic.seek(offset); that.data.bgAudio.isOpen=true; that.setData({ bgAudio:that.data.bgAudio }) }, }) wxss: /* 音频播放器 */ .width{ width:92%; margin: auto; } .flex{ display: flex; align-items: center; flex-wrap: nowrap; } .flex1{ flex:1; } .audio-box{ height:190rpx; background: #fff; padding:30rpx; margin-top:30rpx; border-radius: 20rpx; } .audio-left{ width:64rpx; height:64rpx; } .audio-right{ padding-left:30rpx; } .audio-title{ font-size: 30rpx; font-weight: 400; color: #333333; } .audio-box slider{ padding:0; margin:0; margin-top:10rpx; } wx-slider .wx-slider-handle-wrapper{ height:10rpx; border-radius: 10rpx; } .audio-time{ font-size: 24rpx; font-weight: 400; color: #999999; } wxml <!-- 音频播放器 --> <view class="audio-box width flex"> <text catchtap="{{bgAudio.isOpen? 'ButtonPause':'ButtonPlay'}}">{{bgAudio.isOpen? '暂停':'播放'}}</text> <!-- <image class="audio-left" src="{{bgAudio.isOpen? imagesSrc.icon33:imagesSrc.icon13}}" catchtap="{{bgAudio.isOpen? 'ButtonPause':'ButtonPlay'}}"></image> --> <view class="audio-right flex1"> <view class="audio-title line1">{{bgAudio.title}}</view> <slider bindchange="sliderChangeBj" block-size="14" block-color="#E0C999" step="1" value="{{bgAudio.offset}}" max="{{bgAudio.max}}" activeColor="#E0C999" /> <view class="audio-time flex"> <view class="flex1">{{bgAudio.starttime}}</view> <view>{{bgAudio.duration}}</view> </view> </view> </view> 效果图: [图片]
2023-07-03 - canvas绘制毛玻璃背景分享海报
最近重新设计了分享海报,用毛玻璃作为背景,使整体更有质感,如果没有用到canvas,毛玻璃效果其实很好实现,给元素添加一个滤镜即可(比如:filter: blur(32px)),但是实践的过程中发现,canvas在IOS端一直没有效果,查了一个文档发现IOS端不支持filter。。。有点想骂人。。(PS:微信官方有关CanvasRenderingContext2D的文档还比较简略,更加详细的文档大家可以移步至https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D) [图片] 没办法,实在是喜欢毛玻璃的效果,决定想想办法迈过这道坎,继续网上查阅资料,查到大概的方法就是 1、用canvas上下文先画一个image,image的src就是要模糊处理的图片路径, 2、用canvas上下文把步骤1的image提取imageData,然后进行高斯模糊,高斯模糊的算法网上有挺多,但是拿来执行运算时间要7,8s,比较不理想,最后我是找到了一个stackblur-canvas的插件,这个插件也适用于H5,这里只需要调用里面高斯模糊算法的方法(stackBlur.imageDataRGBA)即可,执行下来几百毫秒就能出来,跟网上的算法差距还是很明显啊。。 3、最后把已经模糊处理的imageData绘制到位图中即可。 canvas滤镜的问题搞定了,还有一个小问题是开发者工具上面canvas的层级始终会比其他元素高,但在IOS真机上元素是可以布局在canvas上。由于不知道在其他客户端会不会也会像开发者工具那样有层级显示问题。所以保险起见我干脆就把canvas放在在页面可视区域底部,在可视区域用其他页面元素做了一个分享效果图出来。 底下两张图,左边是预览,右边是最终画布生成图片。可以看出wxss的滤镜效果还是会比通过第三方高斯算法处理后的模糊效果来的更自然舒服,不过整体效果也还在可接受范围内。 [图片] [图片] 附上核心代码,感谢阅读 组件shareList.wxml <view class="share-box" wx:if="{{visibility}}"> <!-- style="display:none;"--> <view class="share-view" style="opacity:{{shareView?1:0}}"> <view class="cover-image"> <view class="cover"></view> <image class="image" src="{{shareInfo.imgSrc}}"></image> </view> <view class="content-box"> <view class="detail"> <view class="up"> <view class="expand"> <view class="time" wx:if="{{shareInfo.date}}">{{shareInfo.date}}</view> <view class="place" wx:if="{{shareInfo.place}}">{{shareInfo.place}}</view> </view> <image mode="widthFix" class="image" src="{{shareInfo.imgSrc}}" bind:load="imageLoaded"></image> </view> <view class="middle flex-end-vertical"> <image class="header-img" src="{{shareInfo.avatarUrl}}"></image> <image class="joiner-header-img" wx:if="{{shareInfo.joinerAvatarUrl}}" src="{{shareInfo.joinerAvatarUrl}}"></image> <!--<view class="nickname flex-full">showms</view>--> </view> <view class="down"> <view class="desc"><view class="title" wx:if="{{shareInfo.title}}">#{{shareInfo.title}}#</view>{{shareInfo.content}}</view> </view> </view> </view> </view> <view class="save-tool-bar {{device.isPhoneX?'phx_68':''}} flex-center" style="transform: translateY({{shareView?0:100}}%)"> <view class="op_btn flex-full"> <view class="icon-view" bindtap="close"> <image class="icon" src="/images/share/close.png"></image> </view> <text class="text" bindtap="close">关闭</text> </view> <view class="op_btn flex-full {{!allowSave?'disable':''}}"> <view class="icon-view" bindtap="save"> <image class="icon" src="/images/share/save.png"></image> </view> <text class="text" bindtap="save">保存到相册</text> </view> </view> <!--transform: translateY(100%);--> <canvas class="share-canvas" type="2d" id="myCanvas" style="position:absolute;top:0%;width:100%;height:100%;z-index:1000;transform: translateY(100%);"></canvas> </view> <!--toast--> <toast id="toast"></toast> 组件shareList.wxss @import "/app.wxss"; .share-box{ position: absolute; left: 0; right: 0; top: 0; bottom: 0; } .share-cover{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 1200; background-color: #111; opacity: 0.8; } .share-view{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 300; opacity: 0; transition: opacity .3s; } .share-view .cover-image{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; } .share-view .cover-image .image{ width: 100%; height: 100%; transform: scale(3); filter: blur(32px); } .share-view .cover-image .cover{ position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 120; background-color: rgba(255, 255, 255, .5); } .share-view .content-box{ /*padding: 300rpx 40rpx 40rpx; position: relative;*/ position: fixed; left: 40rpx; right: 40rpx; top: 50%; transform: translateY(-50%); margin-top: -80rpx; z-index: 500; box-sizing: border-box; } .share-view .content-box .detail{ background-color: #fff; box-sizing: border-box; /*padding: 70rpx 30rpx 30rpx;*/ border-radius: 36rpx; overflow: hidden; } .share-view .content-box .detail .up{ box-sizing: border-box; position: relative; } .share-view .content-box .detail .up .image{ width: 100%; /*height: auto;*/ display: block; } .share-view .content-box .detail .up .expand{ position: absolute; right: 20rpx; bottom: 20rpx; z-index: 10; text-align: right; font-size: 22rpx; font-weight: 500; color: #fff; text-shadow: 0px 0px 10rpx rgba(158, 163, 175, 1); } .share-view .content-box .detail .middle{ position: relative; } .share-view .content-box .detail .middle .header-img, .share-view .content-box .detail .middle .joiner-header-img{ width: 100rpx; height: 100rpx; border-radius: 50%; border: 6rpx solid #fff; box-sizing: border-box; margin-left: 24rpx; margin-right: 10rpx; margin-top: -64rpx; position: relative; z-index: 80; display: block; } .share-view .content-box .detail .middle .joiner-header-img{ z-index: 60; margin-left: -60rpx; } .share-view .content-box .detail .middle .nickname{ font-size: 30rpx; font-weight: 500; color: #434343; padding-bottom: 10rpx; display: none; } .share-view .content-box .detail .down{ padding: 10rpx 30rpx 70rpx; } .share-view .content-box .detail .down .title{ font-size: 28rpx; font-weight: 500; color: #303133; margin-bottom: 10rpx; margin-right: 10rpx; display: inline; } .share-view .content-box .detail .down .desc{ /*font-size: 28rpx; font-weight: 500; color: #303133; display: inline;*/ font-size: 28rpx; font-weight: 500; color: #303133; line-clamp: 2; box-orient: vertical; text-overflow: ellipsis; overflow: hidden; /*将对象作为弹性伸缩盒子模型显示*/ display: -webkit-box; /*从上到下垂直排列子元素(设置伸缩盒子的子元素排列方式)*/ -webkit-box-orient: vertical; /*这个属性不是css的规范属性,需要组合上面两个属性,表示显示的行数*/ -webkit-line-clamp: 2; height: 76rpx; } .share-canvas{ } .save-tool-bar{ position: absolute; left: 0; right: 0; bottom: 0; z-index: 1600; border-radius: 40rpx 40rpx 0 0; text-align: center; background-color: #f0f0f0; transform: translateY(100%); transition: transform .3s; } .save-tool-bar .op_btn{ text-align: center; padding: 50rpx 0 20rpx; transition: opacity .3s; } .save-tool-bar .op_btn .icon-view{ padding: 26rpx; background-color: #fff; border-radius: 50%; display: inline-block; margin-bottom: 10rpx; } .save-tool-bar .op_btn .icon{ display: block; width: 48rpx; height: 48rpx; } .save-tool-bar .op_btn .text{ display: block; font-size: 20rpx; font-weight: 400; } 组件shareList.js //获取应用实例 const app = getApp(); const tabbar = require('../../utils/tabbar.js'); const canvasHelper = require('../../utils/canvasHelper'); const fonts = require("../../utils/fonts.js"); let ctx, canvas; Component({ /** * 组件的属性列表 */ properties: {}, /** * 组件的初始数据 */ /*{left: 'rgba(26, 152, 252, 0.8)', right: 'rgba(26, 152, 252, 1)'}*/ data: { visibility: false, paddingLeft: 34, letterSpace: 2, width: 300, height: 380, shareView: false, shareInfo: { /*imgSrc: "cloud://ydw-49d951.7964-ydw-49d951-1259010930/love/images/default/dangao.jpg", avatarUrl: "cloud://test-wjaep.7465-test-wjaep-1259010930/images/ozzW05Gch7jMMhsn1r_SWLGdGtF0/avatarUrl_1668440942555.webp", joinerAvatarUrl: " https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83erg8t0El6jTaZY87icvjR71ww52VMibg8fONBgggRtYHTnR2tXibB0IRQ45dCVgNCX5BRhY0KibjfxjGA/132 ", title: "一起去旅游", content: "这里是内容啊,怎么没写内容呢这里是内容啊,怎么没写内容呢,我特知道当时的回复对方法国发过快速减肥", date: "2022-12-28", place: "四川成都", shareQrcode: "cloud://ydw-49d951.7964-ydw-49d951-1259010930/qrcode/mini-qrcode.jpg"*/ }, allowSave: false, }, ready() { const device = app.getSystemInfo(); this.setData({ device, }); this.toast = this.selectComponent('#toast'); }, /** * 组件的方法列表 */ methods: { save() { const {device} = this.data; let that = this; that.toast.showLoadingToast({text: "保存中", mask: false}); canvasHelper.saveImage( this, canvas, 0, 0, device.screenWidth, device.screenHeight, device.screenWidth * 5, device.screenHeight * 5 ).then(res => { that.toast.hideLoadingToast(); that.toast.showToast({text: "保存成功"}); that.close(); console.log(res); }).catch(res => { console.error("保存失败:", JSON.stringify(res)); that.toast.showToast({text: "保存失败"}); that.toast.hideLoadingToast(); }); }, imageLoaded: function (e) { console.log("图片加载完毕:", e); this.setData({shareView: true}); }, show(shareInfo) { console.log("开始显示画布:", shareInfo); this.setData({ shareInfo }); tabbar.hideTab(this); this.setData({visibility: true}, () => { canvasHelper.init(app, this, "#myCanvas").then(async (res) => { ctx = res.ctx; canvas = res.canvas; this.toast.showLoadingToast({text: "生成中", mask: false}); const {device} = this.data; //加大尺寸 const largerSize = 100; console.log("1.绘制毛玻璃背景图片:", { width: device.screenHeight + largerSize, height: device.screenHeight + largerSize, }); /*await canvasHelper.drawImage( canvas, ctx, shareInfo.imgSrc, -(device.screenHeight - device.screenWidth) / 2.0, -largerSize / 2, device.screenHeight + largerSize, device.screenHeight + largerSize, 190);*/ await canvasHelper.drawBlurImage( canvas, ctx, shareInfo.imgSrc, -(device.screenHeight - device.screenWidth) / 2.0, 0, device.screenHeight, device.screenHeight, 180); console.log("2.绘制毛玻璃覆盖层灰色背景"); canvasHelper.drawRoundRect(ctx, 0, 0, device.screenWidth, device.screenHeight, 0, 'rgba(255, 255, 255, .5)'); console.log("3.绘制内容承载区域"); const leftPadding = 20,//边距20 headerImgHeight = 50,//头像尺寸 descHeight = 40,//内容区域高度 descPaddingTop = 0,//内容区域paddingTop descPaddingBottom = 25,//内容区域paddingBottom adjustHeight = 40;//调节高度,人为设定 const contentWidth = device.screenWidth - leftPadding * 2; const contentHeight = contentWidth + headerImgHeight + descHeight + descPaddingTop + descPaddingBottom; canvasHelper.drawRoundRect( ctx, (device.screenWidth - contentWidth) / 2.0, (device.screenHeight - contentHeight) / 2.0 - adjustHeight, contentWidth, contentHeight, 18, 'rgba(255, 255, 255, 1)' ); console.log("4.绘制内容区域图片"); ctx.clip();//裁剪后父元素的圆角才会显示 await canvasHelper.drawImage( canvas, ctx, shareInfo.imgSrc, (device.screenWidth - contentWidth) / 2.0, (device.screenHeight - contentHeight) / 2.0 - adjustHeight, contentWidth, contentWidth, 0, ); ctx.restore(); console.log("5.绘制头像边框"); const headerSize = 50, borderWidth = 3, headerMarginLeft = 12; if (shareInfo.joinerAvatarUrl) { console.log("5.1.绘制共享对象头像"); await canvasHelper.drawCircleImage( canvas, ctx, shareInfo.joinerAvatarUrl, leftPadding + headerMarginLeft + 30, (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth - headerSize / 2, headerSize, borderWidth, "#fff", ); console.log("5.2.绘制当前用户头像"); await canvasHelper.drawCircleImage( canvas, ctx, shareInfo.avatarUrl, leftPadding + headerMarginLeft, (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth - headerSize / 2, headerSize, borderWidth, "#fff", ); } else { console.log("5.1.绘制当前用户头像"); await canvasHelper.drawCircleImage( canvas, ctx, shareInfo.avatarUrl, leftPadding + headerMarginLeft, (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth - headerSize / 2, headerSize, borderWidth, "#fff", ); } console.log("6.绘制日期和地点"); let textPositionY = (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth - headerSize / 2 + 14; if (shareInfo.place) { console.log("6.1.绘制地点"); canvasHelper.drawText( ctx, shareInfo.place, leftPadding + contentWidth - 10, textPositionY, "right", 11, 400, fonts.list["SFRounded-Regular"].name, "#fff", "rgba(158, 163, 175, 1)", 0, 0, 5); textPositionY = textPositionY - 16 } if (shareInfo.date) { console.log("6.2.绘制日期"); canvasHelper.drawText( ctx, shareInfo.date, leftPadding + contentWidth - 10, textPositionY, "right", 11, 400, fonts.list["SFRounded-Regular"].name, "#fff", "rgba(158, 163, 175, 1)", 0, 0, 5); } if (shareInfo.title || shareInfo.desc) { console.log("7.绘制标题和内容", contentWidth); let leftContent = (shareInfo.title ? "#" + shareInfo.title + "# " : "") + shareInfo.content; //显示区域宽度 const displayWidth = contentWidth - 40; textPositionY = (device.screenHeight - contentHeight) / 2.0 - adjustHeight + contentWidth + headerSize; const contentFontSize = 14, contentFontWeight = 600; for (let i = 1; i <= 2; i++) { let dynamicText = ""; for (let j = 0; j < leftContent.length;) { ctx.font = contentFontWeight + " " + contentFontSize + "px " + fonts.list["SFRounded-Semibold"].name; let metrics = ctx.measureText(dynamicText + leftContent[j]); if (metrics.width >= displayWidth) { //最后一行,最后一个字替换成省略号 if (i === 2) { dynamicText = dynamicText.substring(0, dynamicText.length - 1); dynamicText += "…" } break; } dynamicText += leftContent[j]; leftContent = leftContent.slice(1); } //console.log("文本内容:", dynamicText); canvasHelper.drawText( ctx, dynamicText, leftPadding + 20, textPositionY, "left", contentFontSize, contentFontWeight, fonts.list["SFRounded-Semibold"].name, "#303133", "", 0, 0, 0 ); textPositionY = textPositionY + 20; } } console.log("8.绘制二维码"); const qrcodeSize = 66, qrcodeHolderSize = 70; canvasHelper.drawRoundRect( ctx, (device.screenWidth - qrcodeHolderSize) / 2.0, (device.screenHeight - contentHeight) / 2.0 + contentHeight, qrcodeHolderSize, qrcodeHolderSize, 10, 'rgba(255, 255, 255, 1)' ); ctx.clip(); await canvasHelper.drawImage( canvas, ctx, shareInfo.shareQrcode, (device.screenWidth - qrcodeSize) / 2.0, (device.screenHeight - contentHeight) / 2.0 + contentHeight + 2, qrcodeSize, qrcodeSize, 0, ); ctx.restore(); /** await canvasHelper.drawCircleImage( canvas, ctx, shareInfo.shareQrcode, (device.screenWidth - qrcodeSize) / 2.0, (device.screenHeight - contentHeight) / 2.0 + contentHeight, qrcodeSize, 0 );*/ ctx.fill(); this.setData({allowSave: true}); this.toast.hideLoadingToast(); }); }); }, /** * 下载头像 * @param avatarUrl * @returns {Promise} * @private */ _downloadHeaderImg(avatarUrl) { return new Promise(((resolve, reject) => { wx.downloadFile({ url: avatarUrl, //下载头像 success(res) { console.log("下载结束:", res); if (res.statusCode === 200) { //res.tempFilePath resolve(res); } }, fail(res) { reject(res); } }); })); }, /** * 关闭分享页面 */ close() { this.toast.hideLoadingToast(); this.setData({visibility: false, shareView: false, allowSave: false}); tabbar.showTab(this); }, } }) ; 工具类canvasHelper const stackBlur = require("stackblur-canvas"); /** * 初始化画布 * @param app * @param base * @param canvasId * @returns {Promise} */ const init = (app, base, canvasId) => { return new Promise((resolve, reject) => { const device = app.getSystemInfo(); const query = base.createSelectorQuery(); query.select(canvasId) .fields({node: true, size: true}) .exec((res) => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const dpr = device.pixelRatio; canvas.width = res[0].width * dpr; canvas.height = res[0].height * dpr; ctx.scale(dpr, dpr); console.log("画布初始化完毕,画布宽:", canvas.width, "画布高:", canvas.height, "设备像素比:", dpr); resolve({ctx, canvas}); }); }); } /** * 绘制圆角矩形 * @param ctx * @param {number} x 圆角矩形选区的左上角 x坐标 * @param {number} y 圆角矩形选区的左上角 y坐标 * @param {number} w 圆角矩形选区的宽度 * @param {number} h 圆角矩形选区的高度 * @param {number} r 圆角的半径 * @param {string} f 填充颜色 */ const drawRoundRect = (ctx, x, y, w, h, r, f) => { ctx.save(); // 开始绘制 ctx.beginPath(); // 因为边缘描边存在锯齿,最好指定使用 transparent 填充 // 这里是使用 fill 还是 stroke都可以,二选一即可 ctx.fillStyle = f; // ctx.setStrokeStyle('transparent') // 左上角 ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5); // border-top ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.lineTo(x + w, y + r); // 右上角 ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2); // border-right ctx.lineTo(x + w, y + h - r); ctx.lineTo(x + w - r, y + h); // 右下角 ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5); // border-bottom ctx.lineTo(x + r, y + h); ctx.lineTo(x, y + h - r); // 左下角 ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI); // border-left ctx.lineTo(x, y + r); ctx.lineTo(x + r, y); // 这里是使用 fill 还是 stroke都可以,二选一即可,但是需要与上面对应 ctx.fill(); // ctx.stroke() ctx.closePath(); // 剪切 //ctx.clip(); }; /** * 绘制圆形 * @param ctx * @param circlePointX * @param circlePointY * @param radius * @param backgroundColor * @param save */ const drawCircle = (ctx, circlePointX, circlePointY, radius, backgroundColor, save = true) => { if (save) ctx.save(); //ctx.save(); ctx.beginPath(); ctx.arc(circlePointX, circlePointY, radius, 0, 2 * Math.PI, false); ctx.fillStyle = backgroundColor; ctx.fill(); ctx.clip(); //ctx.restore(); if (save) ctx.restore(); }; /** * 绘制圆形白边图像 * @param canvas * @param ctx 图像 * @param imageUrl 图像 * @param startPositionX x坐标 * @param startPositionY y坐标 * @param size 图像大小,除以2就是圆半径 * @param borderWidth 边框宽度 * @param borderColor 边框颜色 * @returns {Promise<*>} * @private */ const drawCircleImage = (canvas, ctx, imageUrl, startPositionX, startPositionY, size, borderWidth, borderColor) => { return new Promise((resolve, reject) => { //叠加圆形的绘制,需要先保存一下原始环境,然后绘制完第一个圆形后恢复绘制环境,即ctx.restore(); ctx.save(); drawCircle( ctx, startPositionX + size / 2, startPositionY + size / 2, size / 2, borderColor, false ); if (borderWidth) { drawCircle( ctx, startPositionX + size / 2, startPositionY + size / 2, size / 2 - borderWidth, borderColor, false ); } drawImage( canvas, ctx, imageUrl, startPositionX, startPositionY, size, size, 0, ).then(res => { ctx.restore(); resolve(res); }).catch(res => { reject(res); }); }); }; /** * 绘制高斯模糊效果的图像 * @param canvas * @param ctx * @param imageUrl * @param startPositionX * @param startPositionY * @param width * @param height * @param blur * @returns {Promise} * @private */ const drawBlurImage = (canvas, ctx, imageUrl, startPositionX, startPositionY, width, height, blur) => { return new Promise((resolve, reject) => { wx.getImageInfo({ src: imageUrl,//服务器返回的图片地址 success: function (res) { console.log("=>", res); let imgObj = canvas.createImage(); imgObj.src = res.path; imgObj.onload = async function (e) { console.log("=========>", imgObj.width, imgObj.height) ctx.save(); ctx.beginPath(); ctx.drawImage(imgObj, startPositionX, startPositionY, width, height); //提取图片信息 let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); //进行高斯模糊 let gd = stackBlur.imageDataRGBA(imageData, 0, 0, canvas.width, canvas.height, blur); //绘制模糊图像 ctx.putImageData(gd, 0, 0) ctx.restore(); console.log("图片加载完毕:", e); resolve(); }; }, fail: function (res) { console.log(res); reject(res); } }); }) }; /** * 绘制文本 * @param ctx * @param text * @param startPositionX * @param startPositionY * @param textAlign * @param fontSize * @param fontWeight * @param color * @param shadowColor 阴影颜色 * @param shadowOffsetX 阴影水平偏移距离 * @param shadowOffsetY 阴影垂直偏移距离 * @param shadowBlur 模糊程度 * @param letterSpace 间隔 * @returns {number} * @private */ const drawText = (ctx, text, startPositionX, startPositionY, textAlign, fontSize, fontWeight, fontFamily, color, shadowColor, shadowOffsetX, shadowOffsetY, shadowBlur, letterSpace = 0) => { if (!text) { return 0; } let textWidth = 0; ctx.save(); ctx.beginPath(); ctx.fillStyle = color; ctx.textAlign = textAlign; ctx.font = fontWeight + " " + fontSize + "px " + fontFamily; if (shadowColor) { console.log("设置阴影:", shadowColor); // 设置阴影 ctx.shadowColor = shadowColor; //阴影颜色 ctx.shadowOffsetX = shadowOffsetX; //偏移 ctx.shadowOffsetY = shadowOffsetY; ctx.shadowBlur = shadowBlur; //模糊程度 } if (!letterSpace) { let metrics = ctx.measureText(text); console.log("文字[" + text + "]宽度:", metrics.width); ctx.fillText( text, startPositionX, startPositionY ); textWidth = metrics.width; } else { //对齐方式调整为left ctx.textAlign = "left"; let positionXArr = [];//坐标集合 textWidth = ctx.measureText(text).width + (text.length - 1) * letterSpace;//含letterSpace的文字总宽度 for (let i = 0; i < text.length; i++) { if (i === 0) { switch (textAlign) { case "left": positionXArr.push(startPositionX); break; case "center": positionXArr.push(startPositionX - textWidth / 2); break; case "right": positionXArr.push(startPositionX - textWidth); break; default: console.warn("暂不支持的textAlign:", textAlign); break; } } else { let metrics = ctx.measureText(text[i - 1]); positionXArr.push(positionXArr[i - 1] + metrics.width + letterSpace); } } for (let i = 0; i < text.length; i++) { ctx.fillText( text[i], positionXArr[i], startPositionY ); } } ctx.restore(); return textWidth; }; /** * 绘制图图像 * @param canvas * @param ctx * @param startPositionX * @param startPositionY * @param width * @param height * @param blur * @param imageUrl * @returns {Promise} * @private */ const drawImage = (canvas, ctx, imageUrl, startPositionX, startPositionY, width, height, blur) => { return new Promise((resolve, reject) => { wx.getImageInfo({ src: imageUrl,//服务器返回的图片地址 success: function (res) { console.log("=>", res); let imgObj = canvas.createImage(); imgObj.src = res.path; imgObj.onload = function (e) { ctx.save(); ctx.beginPath(); ctx.filter = 'blur(' + blur + 'px)'; // ctx.globalAlpha = 0.6 ctx.drawImage(imgObj, startPositionX, startPositionY, width, height); ctx.restore(); console.log("图片加载完毕:", e); resolve(res); }; }, fail: function (res) { console.log(res); reject(res); } }); }) }; /** * 将画布内容保存为图片 * @param base * @param canvas * @param x * @param y * @param width * @param height * @param destWidth * @param destHeight * @param quality * @param fileType * @returns {Promise} */ const saveImage = (base, canvas, x, y, width, height, destWidth, destHeight, quality = 1, fileType = "png") => { return new Promise((resolve, reject) => { wx.canvasToTempFilePath({ x, y, width, height, destWidth, destHeight, quality, fileType, canvas, success(res) { console.log(res.tempFilePath); wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: (res) => { resolve(res); }, fail: (err) => { console.error(err) reject(err); } }) }, fail(res) { console.error("保存失败:", JSON.stringify(res)); reject(res); } }, base) }); } module.exports = { init, drawCircle, drawCircleImage, drawImage, drawBlurImage, drawText, drawRoundRect, saveImage }
2023-05-30