uniapp カスタム透かしカメラ

背景

前回の記事では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 アイコン ライブラリからダウンロードされます。
今日の記事は以上です。少しでもお役に立てれば幸いです。私の記事が気に入っていただけましたら、いいね、コメント、フォローをお願いします。皆さん、ありがとうございました!

おすすめ

転載: blog.csdn.net/abs625/article/details/132831755