canvas绘制毛玻璃背景分享海报

发布于 3 年前作者 oyi5030 次浏览最后编辑 3 年前来自 share

最近重新设计了分享海报,用毛玻璃作为背景,使整体更有质感,如果没有用到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;
                        }
         

...

3 回复
xia47
xia471 楼3 年前

canvas 生成图片确实有些麻烦,推荐一下我做的专门生成海报图的服务 Foolstack刚好发了一篇介绍文章,可以尝试一下。

jiangqiang
jiangqiang2 楼3 年前

楼主 可以整个代码片段体验一下吗?谢谢了

etao
etao3 楼1 个月前

感谢分享,给你点赞