uniapp自定义水印相机

背景

上一篇文章实现了uniapp中给页面添加水印,今天我们实现一个自定义水印相机(最近跟水印杠上了,哈哈)。主要使用了camera组件来实现取景框预览,最后用canvas将自定义水印绘制到拍好的照片上面,先上图镇楼。
在这里插入图片描述在这里插入图片描述

实现

页面分为取景框和拍照完成后预览

UI实现

1、我们首先打开微信,哈哈哈哈,没错就是打开微信,这个取景框页面参考微信拍照界面UI。
2、取景界面分为上下两个部分,上部分为camera取景框组件,下部分为操作区域。

取景框组件上的关闭和水印,以及拍完照片后的略缩图展示需要通过cover-viewcover-image来展示。
代码如下:

<camera :device-position="position" 
        :flash="flash"
        @error="error"
        class="camera" 
        :style="'height:'+cHei+'px;'">
        <cover-image 
            class="close-img active" 
            @click="close" 
            src="/static/miniprogram/custom-camera/close.png"/>
        <cover-view class="water-marker">
            <cover-view class="time">11:09</cover-view>
            <cover-view class="div"></cover-view>
            <cover-view class="date">2023-09-11</cover-view>
            <cover-view class="week">星期一</cover-view>
            <cover-view class="address">江西省南昌市东湖区广场北路2</cover-view>
        </cover-view>
        <cover-image 
            class="result"
            @click="handlePre" 
            :src="photo"
            v-show="photo"/>
    </camera>

下半部分操作区域包含左边闪光灯控制按钮,中间拍照按钮(通过css实现同心圆)以及右边切换摄像头按钮。
实现代码如下:

<view class="bottom layout-row less-center">
        <image :src="'/static/miniprogram/custom-camera/light-'+(flash == 'off' ? 'on' : 'off')+'.png'"
            :class="'light-img active '+(position == 'front' ? 'hidden' : '')"
            @click="handleLight"/>
        <view class="layout-row center cicle-outside">
            <view class="cicle-inside active"
            @click="doTakePhoto"/>
        </view>
        <image src="/static/miniprogram/custom-camera/switch.png"
            class="switch-img active"
            @click="handlePosition"/>
    </view>

这波操作过后,取景页面就是开头你看到的效果了。

功能实现

先定义好控制变量

...
const position = ref('back')//摄像头
const flash = ref('off')//闪光灯
const photo = ref('')//拍完后的图片
const canvasW = ref(0)//绘制水印的canvas宽度
const canvasH = ref(0)//绘制水印的canvas高度
const fristTimedraw = ref(true)//是否为首次绘制
const working = ref(false)//是否正在生成水印
...

关闭按钮事件

...
const close = () => {
        uni.navigateBack({
            delta: 1
        })
    }
...

摄像头切换事件

...
const handlePosition = () => {
        if(working.value){
            return
        }
        if(position.value == 'back'){
            position.value = 'front'
            //切换成前置摄像头关闭闪光灯
            flash.value = 'off'
        }else {
            position.value = 'back'
        }
    }
...

闪光灯事件

...
const handleLight = () => {
        if(working.value){
            return
        }
        if(flash.value == 'off'){
            flash.value = 'on'
        }else {
            flash.value = 'off'
        }
    }
...

拍完后预览

...
const handlePre = () => {
        if(working.value){
            return
        }
        uni.previewImage({
            current: 0,
            urls: [photo.value],
            success: (res) => {
                console.log(res);
            },
        });
    }
...

最终要的拍照功能来了,因为我们camera组件拍出的照片本身是不带水印的, 所以我们需要在界面上放一个canvas组件,后期将水印和图片都绘制到canvas里面,再通过canvas来生成图片。

先在界面上添加一个canvas,且此canvas在界面不可见

...
<canvas canvas-id="firstCanvas"
        class="canvas"
        :style="'width:'+canvasW+'px;height: '+canvasH+'px'"/>
...

全部实现代码

然后就是拍照之后,将照片和水印相关内容绘制到canvas上面,这里包括绘制矩形,文字等,canvas绘图可参考我之前文章canvas绘图API。绘制好了之后通过uni.canvasToTempFilePath将canvas生成位图片。

在这里插入图片描述

我知道的,谁愿意看你瞎BB这么多,有没有能一键复制的全部代码?哎,别走啊,必须安排!!!
接下来就是全部实现代码

<template>
  <view class="layout-column">
    <camera :device-position="position" 
        :flash="flash"
        @error="error"
        class="camera" 
        :style="'height:'+cHei+'px;'">
        <cover-image 
            class="close-img active" 
            @click="close" 
            src="/static/miniprogram/custom-camera/close.png"/>
        <cover-view class="water-marker">
            <cover-view class="time">11:09</cover-view>
            <cover-view class="div"></cover-view>
            <cover-view class="date">2023-09-11</cover-view>
            <cover-view class="week">星期一</cover-view>
            <cover-view class="address">江西省南昌市东湖区广场北路2</cover-view>
        </cover-view>
        <cover-image 
            class="result"
            @click="handlePre" 
            :src="photo"
            v-show="photo"/>
    </camera>
    <view class="bottom layout-row less-center">
        <image :src="'/static/miniprogram/custom-camera/light-'+(flash == 'off' ? 'on' : 'off')+'.png'"
            :class="'light-img active '+(position == 'front' ? 'hidden' : '')"
            @click="handleLight"/>
        <view class="layout-row center cicle-outside">
            <view class="cicle-inside active"
            @click="doTakePhoto"/>
        </view>
        <image src="/static/miniprogram/custom-camera/switch.png"
            class="switch-img active"
            @click="handlePosition"/>
    </view>
    <canvas canvas-id="firstCanvas"
        class="canvas"
        :style="'width:'+canvasW+'px;height: '+canvasH+'px'"/>
  </view>
</template>

<script setup lang="ts">
    import {
        onLoad
    } from "@dcloudio/uni-app";
    import { 
        ref
    } from 'vue'
    const cHei = ref(0)
    const position = ref('back')
    const flash = ref('off')
    const photo = ref('')
    const canvasW = ref(0)
    const canvasH = ref(0)
    const fristTimedraw = ref(true)
    //是否正在生成水印
    const working = ref(false)
    onLoad(() => {
        cHei.value = uni.getSystemInfoSync().windowHeight - uni.upx2px(300)
    })
    const close = () => {
        uni.navigateBack({
            delta: 1
        })
    }
    const error = (e) => {
        console.log('camera error',e)
    }
    const takePhoto = () => {
        const ctx = uni.createCameraContext();
        ctx.takePhoto({
            quality: 'high',
            success: (res) => {
                console.log('takePhoto success',res)
                drawPhoto(res.tempImagePath)
            }
        });
    }
    const doTakePhoto = () => {
        working.value = true
        takePhoto()
        //这里真机上面第一次绘制水印图片可能需要很久,所以延迟500毫秒再执行一次
        if(fristTimedraw.value){
            setTimeout(()=>{
                takePhoto()
                fristTimedraw.value = false
            },500)
        }
    }
    const drawPhoto = (path) => {
        uni.getImageInfo({
            src: path,
            success: res => {
                let ctx = uni.createCanvasContext('firstCanvas');
                //设置画布宽高
                canvasW.value = res.width
                canvasH.value = res.height
                ctx.drawImage(path, 0, 0, res.width, res.height)
                //水印框的大小
                let w = 460
                let h = 180
                //水印框左上角坐标
                let x = 30
                let y = res.height - 210
                //圆角半径
                let r = 20
                let time = "14:30"
                let date = "2023-09-12"
                let week = "星期二"
                let address = "江西省南昌市东湖区广场北路2号"
                ctx.beginPath()
                // 因为边缘描边存在锯齿,最好指定使用 transparent 填充
                ctx.setFillStyle('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()
                ctx.setFillStyle('rgba(255, 255, 255, 0.2)')
                ctx.fillRect(x, y, w, h)
                
                //字体加粗真机不起作用?
                //ctx.font = "normal bold 50px Arial"
                ctx.setFontSize(55); // 设置字体大小
                ctx.setFillStyle('#FFFFFF'); // 设置颜色为白色
                ctx.fillText(time, x+30, y+70);

                let timeW = ctx.measureText(time).width
                ctx.setFillStyle('#FFFF00'); // 设置颜色为
                ctx.fillRect(x+30+timeW+30, y+20, 8, 70)

                ctx.setFontSize(30); // 设置字体大小
                ctx.setFillStyle('#FFFFFF'); // 设置颜色为白色
                ctx.fillText(date, x+30+timeW+30+50, y+45);

                ctx.setFontSize(28); // 设置字体大小
                ctx.setFillStyle('#FFFFFF'); // 设置颜色为白色
                ctx.fillText(week, x+30+timeW+30+50, y+85);

                ctx.setFontSize(24); // 设置字体大小
                ctx.fillText(address, 60, y+140);

                ctx.draw(false, () => {
                    uni.showLoading({
                        title: '正在生成水印照片'
                    })
                    uni.canvasToTempFilePath({
                        canvasId: 'firstCanvas',
                        destWidth: canvasW.value*2,   //展示图片尺寸=画布尺寸1*像素比2
                        destHeight: canvasH.value*2,
                        success: res1 => {
                            working.value = false
                            uni.hideLoading()
                            photo.value = res1.tempFilePath
                        }
                    });
                })
            }
        })
    }
    //照片保存到相册
    const savePhoto = (path) => {
        uni.saveImageToPhotosAlbum({
			filePath: path,
			success: res=> {
				uni.showToast({
                    title: '照片已保存到相册',
                    icon: 'none',
                    duration: 2000
                });
			}
		})
    }
    const handleLight = () => {
        if(working.value){
            return
        }
        if(flash.value == 'off'){
            flash.value = 'on'
        }else {
            flash.value = 'off'
        }
    }
    const handlePosition = () => {
        if(working.value){
            return
        }
        if(position.value == 'back'){
            position.value = 'front'
            //切换成前置摄像头关闭闪光灯
            flash.value = 'off'
        }else {
            position.value = 'back'
        }
    }
    const handlePre = () => {
        if(working.value){
            return
        }
        uni.previewImage({
            current: 0,
            urls: [photo.value],
            success: (res) => {
                console.log(res);
            },
        });
    }
</script>

<style scoped lang="scss">
    page {
        width: 100%;
        height: 100%;
    }
    .camera {
        width: 100%;
        background: #999999;
    }
    .close-img {
        width: 48rpx;
        height: 48rpx;
        margin-top: 110rpx;
        margin-left: 40rpx;
    }
    .light-img {
        width: 48rpx;
        height: 48rpx;
    }
    .switch-img {
        width: 57rpx;
        height: 48rpx;
    }
    .bottom {
        width: 100%;
        height: 300rpx;
        background: black;
        justify-content: space-around;
    }
    .cicle-outside {
        width: 150rpx;
        height: 150rpx;
        border: 5rpx solid #fff;
        border-radius: 50%;
    }
    .cicle-inside {
        width: 130rpx;
        height: 130rpx;
        border-radius: 50%;
        background: #fff;
    }
    .hidden {
        visibility: hidden;
    }
    .water-marker {
        position: absolute;
        left: 30rpx;
        bottom: 30rpx;
        width: 430rpx;
        height: 180rpx;
        background: rgba($color: #ffffff, $alpha: 0.2);
        border-radius: 20rpx;
    }
    .time {
        font-size: 55rpx;
        color: white;
        position: absolute;
        top: 20rpx;
        left: 30rpx;
    }
    .div {
        border-radius: 3rpx;
        width: 8rpx;
        height: 70rpx;
        background: yellow;
        position: absolute;
        top: 20rpx;
        left: 200rpx;
    }
    .date {
        font-size: 28rpx;
        color: white;
        position: absolute;
        top: 20rpx;
        left: 240rpx;
        width: 180rpx;
    }
    .week {
        font-size: 28rpx;
        color: white;
        position: absolute;
        top: 60rpx;
        left: 240rpx
    }
    .address {
        font-size: 24rpx;
        color: white;
        position: absolute;
        top: 120rpx;
        left: 30rpx;
        bottom: 30rpx;
        word-break: break-all;
		word-wrap: break-word;
		white-space: pre-line;
    }
    .canvas {
        position: absolute;
        top: -999999rpx;
        width: 100%;
    }
    .result{
        width: 100rpx;
        height:100rpx;
        position: absolute;
        right:30rpx;
        bottom:30rpx;
        background:white;
        border-radius: 50%;
    }
</style>

里面有些样式是全局实现了引用的,其实就是flex布局然后row或者column,自己实现即可。目前为止,所有功能就已经实现了。

尾巴

页面上用到的图标是从阿里巴巴iconfont图标库下载。
今天的文章就到这里了,希望能给大家帮助,如果喜欢我的文章,欢迎给我点赞,评论,关注,谢谢大家!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值