背景
前回の記事ではuniapp でページにウォーターマークを追加する実装をしましたが、今日はカスタム ウォーターマーク カメラを実装します (最近ウォーターマークで苦労しています、笑)。カメラコンポーネントは主にビューファインダーのプレビューを実現するために使用され、最後にキャンバスは撮影した写真にカスタムウォーターマークを描画するために使用されます。
成し遂げる
ページはビューファインダーと撮影後のプレビューに分かれています。
UIの実装
1. まず WeChat を開きます。ははは、そうです、WeChat を開きます。このビューファインダー ページは WeChat フォト インターフェイス UI を参照しています。
2. ビューファインダーインターフェイスは上下に分かれており、上部はカメラのファインダー枠部分、下部は操作領域です。
ビューファインダー コンポーネントのクロージャと透かし、および写真撮影後のサムネイル表示は、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);
},
});
}
...
最後のカメラ関数はここです。カメラ コンポーネントで撮影した写真にはウォーターマークがないため、インターフェースにキャンバス コンポーネントを配置する必要があります。後で、ウォーターマークと画像の両方をキャンバスに描画し、それらを生成しますキャンバスの絵を通して。
まず、キャンバスをインターフェースに追加します。このキャンバスはインターフェースには表示されません。
...
<canvas canvas-id="firstCanvas"
class="canvas"
:style="'width:'+canvasW+'px;height: '+canvasH+'px'"/>
...
すべての実装コード
写真を撮った後、四角形やテキストなどの描画を含む、写真と透かし関連のコンテンツをキャンバスに描画します。キャンバスの描画については、以前の記事「キャンバス描画 API 」を参照してください。描画後、uni.canvasToTempFilePathを使用してキャンバスからビットマップを生成します。
あなたがふざけているのを誰がそんなに見たいのでしょう? ワンクリックでコピーできるコードはありますか? おい、行かないで、手配をしなければならないんだ!!!
次にすべての実装コードです
<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>
一部のスタイルはグローバルに参照されており、実際にはフレックス レイアウトと行または列であり、自分で実装できます。ここまでで、すべての機能が実装されました。
しっぽ
ページで使用されているアイコンは、Alibaba iconfont アイコン ライブラリからダウンロードされます。
今日の記事は以上です。少しでもお役に立てれば幸いです。私の記事が気に入っていただけましたら、いいね、コメント、フォローをお願いします。皆さん、ありがとうございました!