uniapp自定义水印相机

news2024/11/25 18:27:49

uniapp自定义水印相机

  • 背景
  • 实现
    • UI实现
    • 功能实现
    • 全部实现代码
  • 尾巴

背景

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

实现

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

UI实现

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

取景框组件上的关闭和水印,以及拍完照片后的略缩图展示需要通过cover-view和cover-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图标库下载。
今天的文章就到这里了,希望能给大家帮助,如果喜欢我的文章,欢迎给我点赞,评论,关注,谢谢大家!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1003965.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为云云耀云服务器L实例评测 | 使用UnixBench对华为云云耀云服务器L实例性能测试

文章目录 1 云耀云服务器L实例1.1 简介1.2 使用详情页&#xff1a;远程登录&#xff1a;本地连接&#xff1a;1. 重置密码2.设置安全组 MobaXterm本地连接 2 UnixBench性能测试2.1 UnixBench 的介绍和使用方法2.2 CentOS 7.6系统中编译和安装UnixBench的步骤&#xff1a;2.3 测…

java导出Mysql表信息生成Word文档

一、背景描述 系统上线或者交付&#xff0c;或者需要提供整理数据库表信息&#xff0c;如果一个个整理未免麻烦&#xff0c;接下来一个demo示例如何用JAVA导出Mysql数据库表信息生成Word文档。 传入null导出全部表 传指定表只导出指定表

pmp项目管理考试是什么?适合哪些人学?

PMP&#xff0c;简单点说&#xff0c;就是美国PMI为考察项目管理人士的专业能力而设立的考试。 该流程以知识和任务驱动型指南评估从业者的能力&#xff0c;同时确定项目经理能力行业标准&#xff0c;包括各项知识、任务和技能的特点、重要性与运用频率。&#xff08;考纲原文…

OpenHarmony创新赛|赋能直播第三期

开放原子开源大赛OpenHarmony创新赛赋能直播间持续邀请众多技术专家一起分享应用开发技术知识&#xff0c;本期推出OpenHarmony应用开发之音视频播放器和三方库的使用和方法&#xff0c;助力开发者掌握多媒体应用技术的开发能力和使用三方库提升应用开发的效率和质量&#xff0…

基于springboot的新闻门户网站

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

jenkins+newman+postman持续集成环境搭建

目录 一、Newman简介 二、Newman应用 三、安装newman 四、Html报告插件安装 五、安装nodejs&#xff1a; 六、 Jenkins集成步骤 一、Newman简介 Newman是一款基于Node.js开发的&#xff0c;可以运用postman工具直接从命令运行和测试postman集合 二、Newman应用 环境准备…

低代码开发趋势:利用Zoho Creator构建安全可靠的手机应用指南

在数字化时代&#xff0c;手机应用的重要性不言而喻。无论是企业还是个人用户&#xff0c;手机应用已经成为了连接世界、提升效率和改善生活的不可或缺的工具。然而&#xff0c;传统的手机应用开发通常需要大量的时间、资源和技术知识&#xff0c;对于许多人来说&#xff0c;这…

CSS绘制各种三角形

思路 从盒模型来看&#xff0c;如果content宽高为0, border有宽度&#xff0c;那么上下左右的border就会填充满整个盒子&#xff0c;于是四个方向的border都是三在这里插入图片描述 角形形状&#xff0c;这样想要哪边就显示哪边 效果 代码 html <!DOCTYPE html> <…

手写RPC框架--12.异常重试

异常重试 异常重试a.异常重试b.RPC的自我保护1) 介绍2) 实现令牌桶限流器 c.熔断器d.实现服务端的限流e.实现客户端的熔断f.流量隔离1) 介绍2) 实现 异常重试 a.异常重试 1.为什么需要异常重试&#xff1f; 当发起一次 yrpc 调用&#xff0c;去调用远程的一个服务&#xff0c…

【数据结构】包装类简单认识泛型

文章目录 1 包装类1.1 基本数据类型和对应的包装类1.2 装箱和拆箱 2 什么是泛型3 引出泛型3.1 语法 4 泛型类的使用4.1 语法4.2 示例4.3 类型推导(Type Inference) 5 泛型的上界5.1 语法5.2 示例5.3 复杂示例 6 泛型方法6.1 定义语法6.2 示例6.3 使用示例-可以类型推导6.4 使用…

Spring之文件上传下载,jrebel,多文件上传

文件上传,文件下载jrebel&多文件上传 1.文件上传,文件下载 文件上传 1.spring-xml配置多功能视图解析器 2.前端标记表单为多功能表单enctype”mutipart/form-data“ 3.后端可以直接利用mutipartFile类&#xff0c;接受前端传递到后台的文件 4.将文件转成流&#xff0c;然后…

DedeCMS_v5.7其他漏洞复现

一、URL重定向 http://127.0.0.1/DedeCMS-V5.7-UTF8-SP2/uploads/plus/download.php?open1&linkaHR0cDovL3d3dy5iYWlkdS5jb20 其中aHR0cDovL3d3dy5iYWlkdS5jb20是http://www.baidu.com的base64编码 访问后发现直接转到百度 二、后台shops_delivery_存储型XSS 管理员在…

30m退耕还湿空间数据集(2000-2010年,青藏高原地区)

摘要 a. 数据内容&#xff08;数据文件/表名称&#xff0c;包含的观测指标内容&#xff09; 30m退耕还湿空间数据集(2000-2010年&#xff0c;青藏高原地区)&#xff0c;反映了2000年至2010年期间青藏高原地区耕地退耕还湿的空间分布情况。 b. 建设目的 监测青藏高原地区的退耕还…

10 大演讲主题、14 位技术大咖!龙蜥大讲堂 9 月直播预告硬核来袭

「龙蜥大讲堂」9 月精彩预告来了&#xff0c;点击下方海报抢先了解。本月又是满满的技术干货分享&#xff0c;多位大咖带你共享技术盛宴&#xff01;提前进群&#xff0c;参与互动还有龙蜥精美周边等你来拿。 9 月精彩分享直达 &#x1f447; 加入龙蜥社区钉钉交流群&#xff0…

Springboot+swagger2

1.swagger配置 /*** Swagger 配置文件*/ Configuration public class SwaggerConfig {Beanpublic Docket createRestApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.swagger.two&qu…

神经网络 02(激活函数)

一、激活函数 在神经元中引入了激活函数&#xff0c;它的本质是向神经网络中引入非线性因素的&#xff0c;通过激活函数&#xff0c;神经网络就可以拟合各种曲线。 如果不用激活函数&#xff0c;每一层输出都是上层输入的线性函数&#xff0c;无论神经网络有多少层&#xff0c…

华为云云服务器云耀L实例评测 | 智能不卡顿:如何实现流畅的业务运行

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

数据分享|R语言武汉流动人口趋势预测:灰色模型GM(1,1)、ARIMA时间序列、logistic逻辑回归模型...

全文链接&#xff1a;http://tecdat.cn/?p32496 人口流动与迁移&#xff0c;作为人类产生以来就存在的一种社会现象&#xff0c;伴随着人类文明的不断进步从未间断&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09;。 相关视频 人力资源是社会文明进步、人民富裕…

视频直播点播平台EasyDSS如何单独保存录像计划文件?具体如何操作呢?

视频推拉流EasyDSS视频直播点播平台&#xff0c;集视频直播、点播、转码、管理、录像、检索、时移回看等功能于一体&#xff0c;可提供音视频采集、视频推拉流、播放H.265编码视频、存储、分发等视频能力服务。 有用户反馈&#xff1a;在视频直播点播平台EasyDSS中设置了片段形…

this执行问题

1.代码 var a 10;let obj {a: 20,n: function () {console.log(this.a);},};let fn obj.n;fn(); //此时的this指向windowobj.n(); //this指向obj这个对象 2.打印的结果 3.代码分析 let fn obj.n;将函数体复制给fn fn()是普通函数this指向window obj.fn里面的函数,可以理…