小程序实现摄像头拍照 + 水印绘制

news/2024/7/10 0:36:46 标签: 小程序, 前端, uni-app, vue, 水印, 拍照

文章标题

  • 01 功能说明
  • 02 使用方式 & 效果图
  • 03 全部代码
    • 3.1 页面布局 html
    • 3.2 业务核心 js
    • 3.3 基础样式 css

01 功能说明

需求小程序端需要调用前置摄像头进行拍照,并且将拍好的照片添加水印后返回。下面的代码支持 底部定点水印整体背景水印

技术栈uniappvue

迭代:后期还可以继续 扩展多方位的定点水印 和 支持绘制多句话的背景水印

02 使用方式 & 效果图

文件路径:"@/components/CameraSnap.vue"

2.1 基础用法

// (1)仅拍照 + 预览
<CameraSnap />

// (4)仅给图片添加水印 + 预览
<CameraSnap 
	photoSrc="xxx" 
	:mark-list="['今天天气很好','2023-01-01 00:00:00']" 
/>

2.2 拍照 + 底部定点水印 + 预览

使用方式:

<CameraSnap 
	:mark-list="['今天天气很好','2023-01-01 00:00:00']" 
	textSize="24" 
	useTextMask 
/>

效果如下:
<a class=拍照_底部定点水印_预览" />

2.3 拍照 + 整体背景水印 + 预览

使用方式:

// 若不设置 markType,则默认为 底部定点水印
// 目前背景水印只会取 markList 的第一项来绘制背景水印
<CameraSnap 
	markType="background" 
	:mark-list="['今天天气很好']" 
	textColor="rgba(255,255,255,0.5)"  
/>

效果如下:
<a class=拍照_整体背景水印_预览" />

03 全部代码

uni-app camera 的官方文档:https://uniapp.dcloud.net.cn/component/camera.html#camera

3.1 页面布局 html

<template>
	<view class="camera-wrapper">
		<!-- 拍照 -->
		<template v-if="!snapSrc">
			<!-- 相机 -->
			<camera device-position="front" flash="off" @error="handleError" class="image-size">
				<view class="photo-btn" @click="handleTakePhoto">拍照</view>
			</camera>
			<!-- 水印 -->
			<canvas canvas-id="photoMarkCanvas" id="photoMarkCanvas" class="mark-canvas"
				:style="{width: canvasWidth+'px',height: canvasHeight+'px'}" />
		</template>
		<!-- 预览 -->
		<template v-else>
			<view class="re-photo-btn" @click="handleRephotograph">重拍</view>
			<image class="image-size" :src="snapSrc"></image>
		</template>
	</view>
</template>

3.2 业务核心 js

<script>
	export default {
		name: 'CameraSnap',
		props: {
			// 照片地址(若传递了照片地址,则默认为预览该照片或添加水印后预览)
			photoSrc: {
				type: String,
				default: ""
			},
			// 水印类型
			markType: {
				type: String,
				default: "fixed", // 定点水印 fixed,背景水印 background
			},
			// 水印文本列表(支持多行)
			markList: {
				type: Array,
				default: () => []
			},
			textColor: {
				type: String,
				default: "#FFFFFF"
			},
			textSize: {
				type: Number,
				default: 32
			},
			// 定点水印的遮罩(为了让水印更清楚)
			useTextMask: {
				type: Boolean,
				default: true
			}
		},
		data() {
			return {
				snapSrc: "",
				canvasWidth: "",
				canvasHeight: "",
			}
		},
		watch: {
			photoSrc: {
				handler: function(newValue, oldValue) {
					if (newValue) {
						this.getWaterMarkImgPath(newValue)
					}
				},
				immediate: true
			}
		},
		methods: {
			handleTakePhoto() {
				const ctx = uni.createCameraContext();
				ctx.takePhoto({
					quality: 'high',
					success: (res) => {
						const imgPath = res.tempImagePath
						if (this.markList.length) {
							this.getWaterMarkImgPath(imgPath)
						} else {
							this.snapSrc = imgPath;
							console.log("default", this.snapSrc)
							this.$emit('complete', imgPath)
						}
					}
				});
			},
			handleRephotograph() {
				this.snapSrc = ""
			},
			handleError(err) {
				uni.showModal({
					title: '警告',
					content: '若不授权使用摄像头,将无法使用拍照功能!',
					cancelText: '不授权',
					confirmText: '授权',
					success: (res) => {
						if (res.confirm) {
							// 允许打开授权页面,调起客户端小程序设置界面,返回用户设置的操作结果
							uni.openSetting({
								success: (res) => {
									res.authSetting = { "scope.camera": true }
								},
							})
						} else if (res.cancel) {
							// 拒绝打开授权页面
							uni.showToast({ title: '您已拒绝授权,无法进行拍照', icon: 'error', duration: 2500 });
						}
					}
				})
			},
			setWaterMark(context, image) {
				const listLength = this.markList?.length
				switch (this.markType) {
					case 'fixed':
						const spacing = 4 // 行间距
						const paddingTopBottom = 20 // 整体上下间距
						// 默认每行的高度 = 字体高度 + 向下间隔
						const lineHeight = this.textSize + spacing
						const allLineHeight = lineHeight * listLength
						// 矩形遮罩的 Y 坐标
						const maskRectY = image.height - allLineHeight
						// 绘制遮罩层
						if (this.useTextMask) {
							context.setFillStyle('rgba(0,0,0,0.4)');
							context.fillRect(0, maskRectY - paddingTopBottom, image.width, allLineHeight + paddingTopBottom)
						}
						// 文本与 x 轴之间的间隔
						const textX = 10
						// 文本一行的最大宽度(减去 20 是为了一行的左右留间隙)
						const maxWidth = image.width - 20
						context.setFillStyle(this.textColor)
						context.setFontSize(this.textSize)
						this.markList.forEach((item, index) => {
							// 因为文本的 Y 坐标是指文本基线的 Y 轴坐标,所以要获取文本顶部的 Y 坐标
							const textY = maskRectY - paddingTopBottom / 2 + this.textSize + lineHeight * index
							context.fillText(item, textX, textY, maxWidth);
						})
						break;
					case 'background':
						context.translate(0, 0);
						context.rotate(30 * Math.PI / 180);
						context.setFillStyle(this.textColor)
						context.setFontSize(this.textSize)
						const colSize = parseInt(image.height / 6)
						const rowSize = parseInt(image.width / 2)
						let x = -rowSize
						let y = -colSize
						// 循环绘制 5 行 6 列 的文字
						for (let i = 1; i <= 6; i++) {
							for (let j = 1; j <= 5; j++) {
								context.fillText(this.markList[0], x, y, rowSize)
								// 每个水印间隔 20
								x += rowSize + 20
							}
							y += colSize
							x = -rowSize
						}
						break;
				}
				context.save();
			},
			getWaterMarkImgPath(src) {
				const _this = this
				uni.getImageInfo({
					src,
					success: (image) => {
						this.canvasWidth = image.width
						this.canvasHeight = image.height
						const context = uni.createCanvasContext("photoMarkCanvas", this)
						context.drawImage(src, 0, 0, image.width, image.height)
						// 设置水印
						this.setWaterMark(context, image)
						// 若还需其他操作,可在操作之后叠加保存:context.restore()
						// 将画布上的图保存为图片
						context.draw(false, () => {
							setTimeout(() => {
								uni.canvasToTempFilePath({
										destWidth: image.width,
										destHeight: image.height,
										canvasId: 'photoMarkCanvas',
										fileType: 'jpg',
										success: function(res) {
											_this.snapSrc = res.tempFilePath
											console.log("water", _this.snapSrc)
											_this.$emit('complete', _this.snapSrc)
										}
									},
									_this
								);
							}, 200)
						});
					}
				})
			},
		}
	}
</script>

3.3 基础样式 css

<style lang="scss" scoped>
	.camera-wrapper {
		position: relative;
	}

	.mark-canvas {
		position: absolute;
		/* 将画布移出展示区域 */
		top: -200vh;
		left: -200vw;
	}

	.image-size {
		width: 100%;
		height: 100vh;
	}

	.photo-btn {
		position: absolute;
		bottom: 100rpx;
		left: 50%;
		transform: translateX(-50%);
		width: 140rpx;
		height: 140rpx;
		line-height: 140rpx;
		text-align: center;
		background-color: #000000;
		border-radius: 50%;
		border: 10rpx solid #ffffff;
		color: #fff;
	}

	.re-photo-btn {
		position: absolute;
		bottom: 80rpx;
		right: 40rpx;
		padding: 10rpx 20rpx;
		background-color: #000000;
		border-radius: 10%;
		border: 6rpx solid #ffffff;
		color: #fff
	}
</style>

http://www.niftyadmin.cn/n/5011257.html

相关文章

欧洲电子产品CE认证 CE-EMC认证办理

任何的产品想要在欧洲自由贸易必须通过CE认证&#xff0c;在产品上加贴CE标志。CE标志表示产品已经达到了欧盟指令规定的安全要求;是企业对消费者的一种承诺&#xff0c;增加了消费者对产品的信任程度。 贴有CE标志的产品将降低在欧洲市场上销售的风险。这些风险包括&#xff1…

【LeetCode】一起探究三数之和的奥秘

Problem: 15. 三数之和 文章目录 题目解析算法原理分析排序 暴力枚举 set去重排序 单调性 双指针划分思想 复杂度Code 题目解析 首先我们来分析一下本题的思路 题目说到要我们在一个整数数组中去寻找三元组&#xff0c;而且呢这三个数字所相加的和为0&#xff0c;而且呢这三…

LED屏幕电流驱动设计原理

LED电子显示屏作为户外最大的应用产品&#xff0c;是大型娱乐&#xff0c;体育赛事&#xff0c;广场大屏幕等场所不可或缺的产品&#xff0c;从单双色简单的文字展示到今天的高清全彩&#xff0c;显示屏的技术一直都在进步&#xff0c;全球80%的LED电子显示屏皆产自于中国。显示…

第一百三十八回 如何在图标旁边添加小红点

文章目录 概念介绍实现方法示例代码 我们在上一章回中介绍了WebView组件相关的内容&#xff0c;本章回中将介绍 如何在图标旁边添加小红点.闲话休提&#xff0c;让我们一起Talk Flutter吧。 概念介绍 在实际项目中有时候需要在图标旁边显示小红点&#xff0c;而且小红点内还有…

linux使用stress命令进行压力测试cpu

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01; &#x1f40b; 希望大家多多支…

Android lint配置及使用

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、商业变现、人工智能等&#xff0c;希望大家多多支持。 目录 一、导读二、概览三、将 lint 配置为不显示警告3.1 在 A…

Redis Redis介绍、安装 - Redis客户端

目录 redis是什么&#xff0c;他的应用场景是什么&#xff1f; Redis的一些主要特点和应用场景&#xff1a; redis的官方网站&#xff1a;Redis redis是键值型数据库&#xff1a;&#xff08;也就是key-value模式&#xff09;&#xff08;跟python的字典很像&#xff09; …

a_bogus 音 算法还原大赏

a_bogus算法还原大赏 hello&#xff0c;大家好呀&#xff0c;我是你的好兄弟&#xff0c;[星云牛马]&#xff0c;花了几天时间算法还原了这个参数的加密过程&#xff0c;一起看看吧&#xff0c;记得加入我们的学习群&#xff1a;529528142 天才第一步&#xff0c;F12你会不&am…