uniapp canvas图片缩放合成
·
<template>
<view class="containerBox">
<view class="containerCanvas" v-if="tabValue == 0">
<!-- 主容器 -->
<view class="container" ref="container">
<view v-for="img in images" :key="img.id" class="draggable-image" :style="{
left: img.left + 'px',
top: img.top + 'px',
width: img.width + 'px',
height: img.height + 'px',
zIndex: img.active ? 999 : 1
}" @touchstart="startDrag(img, $event)" @touchmove.prevent="onDrag($event)" @touchend="stopDrag">
<image :src="img.src" style="width: 100%; height: 100%;" />
<view class="resize-handle top-left" @touchstart.stop="startResize(img, $event, 'top-left')"
@touchmove.stop.prevent="onDrag($event)"></view>
<view class="resize-handle top-right" @touchstart.stop="startResize(img, $event, 'top-right')"
@touchmove.stop.prevent="onDrag($event)"></view>
<view class="resize-handle bottom-left" @touchstart.stop="startResize(img, $event, 'bottom-left')"
@touchmove.stop.prevent="onDrag($event)"></view>
<view class="resize-handle bottom-right" @touchstart.stop="startResize(img, $event, 'bottom-right')"
@touchmove.stop.prevent="onDrag($event)"></view>
</view>
<canvas id="previewCanvas" :style="{
position: 'absolute',
left: '-9999px',
width: containerRect.width + 'px',
height: containerRect.height + 'px'
}" canvas-id="previewCanvas"></canvas>
</view>
<!-- 图片库 -->
<view @click="deleteImage" class="delBtn" v-if="selectedImage && !selectedImage.isBackground">
<u-icon name="trash" color="#fff" size="26"></u-icon>
</view>
<view class="footer">
<view class="disFlex justify-between">
<u-button type="primary" text="添加" @click="openimgtab" color="#2979ff"
style="width: 25%;margin: 0;"></u-button>
<u-button type="primary" text="保存" @click="capturePage" color="#2979ff"
style="width: 25%;margin: 0;"></u-button>
<u-button type="primary" text="重置" @click="resetImage" color="#e54d42"
style="width: 25%;margin: 0;"></u-button>
</view>
</view>
<!-- 控制面板 -->
<!-- <view class="control-panel" v-if="selectedImage">
<button class="delete-btn" @tap="deleteImage">删除</button>
</view> -->
</view>
</view>
<u-popup :show="imgtab" @close="imgtabclose" :zIndex="100" :round="10" :closeable="true">
<view class="popupBottom text-center" style="padding-bottom: 50rpx;">
<!-- <view class="pop_title">提示</view> -->
<view v-if="!isAmplify" class="pop_title border-bottom" style="padding: 30rpx 0;"
@click="gopage(`/pages/mine/DoorsW?type=change&isData=${bgimage?true:false}`)">门窗素材</view>
<my-popup v-if="isAmplify" ref="mypopup" @getimg="afterdata" :value="bgimage" :deleIcon="false"
:imgsecond="true" :isshowList="false" :count="1" :upcount="1" imgalign="right">
<template v-slot:imgtitle>
<view class="pop_title" style="padding: 30rpx 0;width: 100vw;">建筑背景</view>
</template>
</my-popup>
</view>
</u-popup>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
watch,
getCurrentInstance
} from 'vue'
import {
onLoad,
onShow,
onReady,
onUnload
} from '@dcloudio/uni-app'
import {
toast,
loading
} from "@/hooks/useInterface";
import {
async
} from 'regenerator-runtime';
onUnload(() => {
uni.$off('isRefresh'); // 页面卸载时移除监听
});
const isAmplify = ref(true)
onShow(() => {
// 每次进入页面时先移除旧的监听
uni.$off('isRefresh');
// 重新注册监听
uni.$on('isRefresh', function(data) {
console.log('监听到事件来自返回的参数:' + data);
if (data) {
isAmplify.value = false
addImage(data)
}
})
})
// 响应式数据
const tabValue = ref(0)
const images = ref([])
const selectedImage = ref(null)
const containerRect = ref({
width: 0,
height: 0
})
// 状态变量
let isDragging = false
let isResizing = false
let currentImg = null
let startX = 0
let startY = 0
let initialLeft = 0
let initialTop = 0
let initialWidth = 0
let initialHeight = 0
let resizeDirection = ''
let originalRight = 0
let originalBottom = 0
const imgtab = ref(false)
const bgimage = ref('')
// 图片库数据
const imageLibrary = ref([])
// 初始化容器尺寸
onReady(() => {
const query = uni.createSelectorQuery()
query.select('.container').boundingClientRect(data => {
if (data) {
containerRect.value = {
width: data.width,
height: data.height
}
}
}).exec()
})
const gopage = (e) => {
uni.navigateTo({
url: e
})
imgtabclose()
}
const openimgtab = () => {
imgtab.value = true
}
const imgtabclose = () => {
imgtab.value = false
}
const addImg = (e) => {
// #ifdef APP-PLUS
console.log(plus.os.name);
if (plus.os.name == 'Android') {
setTimeout(() => {
authpup.value.open()
}, 20)
}
// #endif
uni.chooseImage({
count: 1, // 默认9
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
success: (file) => {
proxy.$useFileUpload(file).then(item => {
console.log(item);
addImage(item)
})
}
})
}
const afterdata = async (e) => {
let img = await proxy.$useFileUpload(e)
bgimage.value = img
addImage(img, true)
}
const addImage = (src, isBackground = false) => {
console.log(src, "srcsrcsrcsrc")
uni.getImageInfo({
src,
success: (res) => {
images.value.push({
id: Date.now(),
src: src,
isBackground: isBackground,
left: 20,
top: 50,
// width: 100,
// height: 100 * (res.height / res.width),
width: isAmplify.value ? containerRect.value.width - 50 : 100,
height: isAmplify.value ? containerRect.value.width : 100 * (res.height / res
.width),
active: false
})
isAmplify.value = false
},
fail: () => {
uni.showToast({
title: '图片加载失败',
icon: 'none'
})
}
})
}
// 拖拽逻辑
const isDelShow = ref(false)
const startDrag = (img, event) => {
const touch = event.touches[0]
isDragging = true
currentImg = img
startX = touch.clientX - img.left
startY = touch.clientY - img.top
selectedImage.value = img
}
// 缩放逻辑
const startResize = (img, event, direction) => {
const touch = event.touches[0]
isResizing = true
resizeDirection = direction
currentImg = img
selectedImage.value = img
// 记录初始位置和尺寸
initialLeft = img.left
initialTop = img.top
initialWidth = img.width
initialHeight = img.height
// 计算边界限制值
originalRight = initialLeft + initialWidth
originalBottom = initialTop + initialHeight
// 记录初始触摸点坐标
startX = touch.clientX
startY = touch.clientY
//记录裁剪范围
if (selectedImage.value.isBackground) {
console.log("建筑背景:", img)
bgSize.height = img.newHeight
bgSize.width = img.newWidth
}
console.log('Start resize:', {
direction,
initialLeft,
initialTop,
initialWidth,
initialHeight,
startX: touch.clientX,
startY: touch.clientY
})
}
const onDrag = (event) => {
if (isResizing && currentImg) {
const touch = event.touches[0]
const deltaX = touch.clientX - startX
const deltaY = touch.clientY - startY
const minSize = 50 // 最小尺寸限制
let newWidth = initialWidth
let newHeight = initialHeight
let newLeft = initialLeft
let newTop = initialTop
switch (resizeDirection) {
case 'top-left':
newLeft = Math.max(0, Math.min(initialLeft + deltaX, originalRight - minSize))
newTop = Math.max(0, Math.min(initialTop + deltaY, originalBottom - minSize))
newWidth = originalRight - newLeft
newHeight = originalBottom - newTop
break
case 'top-right':
newWidth = Math.max(minSize, initialWidth + deltaX)
newHeight = Math.max(minSize, initialHeight - deltaY)
newTop = initialTop + deltaY
break
case 'bottom-left':
newWidth = Math.max(minSize, initialWidth - deltaX)
newHeight = Math.max(minSize, initialHeight + deltaY)
newLeft = initialLeft + deltaX
break
case 'bottom-right':
newWidth = Math.max(minSize, initialWidth + deltaX)
newHeight = Math.max(minSize, initialHeight + deltaY)
break
}
// 应用边界限制
newWidth = Math.min(newWidth, containerRect.value.width - newLeft)
newHeight = Math.min(newHeight, containerRect.value.height - newTop)
// 更新图片属性
currentImg.width = newWidth
currentImg.height = newHeight
currentImg.left = newLeft
currentImg.top = newTop
//记录裁剪范围
if (selectedImage.value.isBackground) {
bgSize.height = newHeight
bgSize.width = newWidth
}
console.log('Resizing:', {
direction: resizeDirection,
newWidth,
newHeight,
newLeft,
newTop
})
} else if (isDragging && currentImg) {
const touch = event.touches[0]
const newLeft = touch.clientX - startX
const newTop = touch.clientY - startY
// 边界约束
currentImg.left = Math.max(0, Math.min(
newLeft,
containerRect.value.width - currentImg.width
))
currentImg.top = Math.max(0, Math.min(
newTop,
containerRect.value.height - currentImg.height
))
}
}
// 停止操作
const stopDrag = () => {
isDragging = false
isResizing = false
currentImg = null
}
// 删除图片
const deleteImage = () => {
if (selectedImage.value) {
images.value = images.value.filter(img => img.id !== selectedImage.value.id)
selectedImage.value = null
}
}
//重置
const resetImage = () => {
uni.showModal({
title: '提示',
content: '确认重置画布嘛?',
success: function(res) {
if (res.confirm) {
isAmplify.value = true
images.value = [] // 清空所有图片元素
bgimage.value = '' // 清空背景图
const context = uni.createCanvasContext('previewCanvas', this)
context.clearRect(0, 0, containerRect.value.width, containerRect.value.height)
context.draw() // 确保清空画布操作生效
}
}
})
}
// 保存图片
const bgSize = reactive({
width: 0,
height: 0
})
const capturePage = async () => {
try {
const context = uni.createCanvasContext('previewCanvas', this)
// 清空画布
context.clearRect(0, 0, containerRect.value.width, containerRect.value.height)
// 绘制所有图片
const drawPromises = images.value.map(img => {
return new Promise((resolve) => {
context.drawImage(
img.src,
img.left,
img.top,
img.width,
img.height
)
resolve()
})
})
await Promise.all(drawPromises)
context.draw(false, () => {
uni.canvasToTempFilePath({
canvasId: 'previewCanvas',
x: 20,
y: 50,
width: bgSize.width, // 使用背景图实际宽度
height: bgSize.height, // 使用背景图实际高度
destWidth: bgSize.width,
destHeight: bgSize.height,
quality: 1,
success: (res) => {
// #ifdef H5
saveFlieImage(res.tempFilePath)
// #endif
// #ifdef APP-PLUS
var path = plus.io.convertLocalFileSystemURL(res.tempFilePath); //绝对路径
var fileReader = new plus.io.FileReader();
fileReader.readAsDataURL(path);
fileReader.onloadend = function(evt) { //读取文件成功完成的回调函数
console.log(evt.target.result); //输出base64内容
saveFlieImage(evt.target.result)
}
// #endif
},
fail: (err) => {
console.error('生成临时文件失败:', err)
uni.showToast({
title: '生成图片失败',
icon: 'none'
})
}
})
})
} catch (e) {
console.error('保存出错:', e)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
const saveFlieImage = (savedFilePath) => {
// console.log(savedFilePath)
processBase64Image(savedFilePath).then(result => {
});
}
const processBase64Image = async (base64String) => {
try {
// 分割 Base64 字符串
const [header, data] = base64String.split(';base64,');
const mimeType = header.split(':')[1];
const rawData = atob(data);
// 平台判断
let tempFilePaths = [];
// #ifdef H5
// H5 环境:使用 Blob
const blob = new Blob([new Uint8Array(rawData.length).map((_, i) => rawData.charCodeAt(i))], {
type: mimeType
});
const blobURL = URL.createObjectURL(blob);
tempFilePaths = [blobURL];
// #endif
// #ifdef MP-WEIXIN
// 微信小程序:写入临时文件
const fs = wx.getFileSystemManager();
const filePath = `${wx.env.USER_DATA_PATH}/temp_${Date.now()}.${mimeType.split('/')[1]}`;
fs.writeFileSync(filePath, rawData, 'base64');
tempFilePaths = [filePath];
// #endif
// #ifdef APP
// App 环境:使用原生 API 保存文件
const fileName = `image_${Date.now()}.${mimeType.split('/')[1]}`;
const url = `_doc/${fileName}`;
const bitmap = new plus.nativeObj.Bitmap('temp');
await new Promise((resolve, reject) => {
bitmap.loadBase64Data(base64String, () => {
bitmap.save(url, {
overwrite: true
}, () => {
bitmap.clear();
resolve();
}, (err) => reject(err));
}, (err) => reject(err));
});
tempFilePaths = [url];
// #endif
// 获取图片尺寸(H5/小程序/APP 通用)
const getImageDimensions = (base64) => {
return new Promise(async (resolve) => {
let dimensions = {
width: 0,
height: 0
};
// 平台判断
// #ifdef H5
// H5 环境:使用 Image 对象
const img = new Image();
img.onload = () => resolve({
width: img.width,
height: img.height
});
img.onerror = () => resolve(dimensions);
img.src = base64;
// #endif
// #ifdef MP-WEIXIN
// 微信小程序:使用 wx.getImageInfo
try {
const res = await wx.getImageInfo({
src: base64
});
dimensions = {
width: res.width,
height: res.height
};
} catch (e) {
console.error('获取图片尺寸失败:', e);
}
resolve(dimensions);
// #endif
// #ifdef APP
// App 环境:使用 uni.getImageInfo
try {
const res = await uni.getImageInfo({
src: base64
});
dimensions = {
width: res.width,
height: res.height
};
} catch (e) {
console.error('获取图片尺寸失败:', e);
}
resolve(dimensions);
// #endif
});
};
const dimensions = await getImageDimensions(base64String);
return {
tempFilePaths,
tempFiles: [{
path: tempFilePaths[0],
type: mimeType,
size: rawData.length,
base64: base64String,
name: `image_${Date.now()}.${mimeType.split('/')[1]}`,
...dimensions
}],
errMsg: "chooseImage:ok"
};
} catch (error) {
console.error('处理Base64时出错:', error);
throw error;
}
};
</script>
<style lang="scss">
/* 容器样式 */
// .container {
// position: fixed;
// top: 0;
// left: 0;
// width: 100vw;
// height: 100vh;
// background: #ffffff;
// touch-action: none;
// }
page {
width: 100%;
height: 100%;
}
.containerBox {
width: 100%;
/* #ifdef H5 */
height: calc(100% - 110rpx);
/* #endif */
/* #ifdef APP-PLUS */
height: calc(100% - 178rpx);
/* #endif */
display: flex;
flex-direction: column;
background-color: #fff;
overflow: hidden;
.containerCanvas {
flex: 1;
display: flex;
flex-direction: column;
.container {
width: 100%;
flex: 1;
overflow: hidden;
background: white;
display: flex;
flex-direction: column;
position: relative;
/* 可拖拽图片样式 */
.draggable-image {
position: absolute;
touch-action: none;
border: 2rpx solid #eee;
}
/* 缩放手柄样式 */
.resize-handle {
position: absolute;
width: 40rpx;
height: 40rpx;
background: #fff;
border: 4rpx solid #2196F3;
border-radius: 50%;
&.top-left {
top: -20rpx;
left: -20rpx;
}
&.top-right {
top: -20rpx;
right: -20rpx;
}
&.bottom-left {
bottom: -20rpx;
left: -20rpx;
}
&.bottom-right {
bottom: -20rpx;
right: -20rpx;
}
}
}
.footer {
width: 100%;
flex-shrink: 0;
padding: 20rpx;
background: rgba(240, 240, 240, 0.95);
/* 图片库样式 */
.image-library {
width: 100%;
height: 160rpx;
white-space: nowrap;
margin-top: 20rpx;
.library-image {
display: inline-block;
width: 160rpx;
height: 160rpx;
margin-right: 20rpx;
border-radius: 16rpx;
overflow: hidden;
border: 2rpx solid #ddd;
image {
width: 100%;
height: 100%;
}
}
}
}
}
// 素材区
.canvasBox {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
.navTab {
flex-shrink: 0;
width: 100%;
display: flex;
justify-content: space-between;
width: 100%;
height: 70rpx;
background-color: #fff;
.itemName {
width: 50%;
text-align: center;
line-height: 70rpx;
border-bottom: 4rpx solid #fff;
}
.active {
border-bottom: 4rpx solid #009CFF;
color: #0081ff;
}
}
.navBox {
flex: 1;
width: 100%;
}
}
}
.imageBoxShow {
width: 600rpx;
height: 700rpx;
box-sizing: border-box;
padding: 30rpx;
border-radius: 10rpx;
.imageYu {
width: 100%;
height: 540rpx;
display: block;
margin: 0 auto;
}
.imageBtn {
display: flex;
box-sizing: border-box;
margin-top: 20rpx;
}
}
.popupBottom {
width: 100%;
/* height: 650rpx; */
background: #fff;
box-sizing: border-box;
border-radius: 20rpx;
padding: 40rpx 0rpx;
font-size: 28rpx;
position: relative;
margin: auto;
}
.pop_title {
font-size: 30rpx;
font-weight: 500;
}
.delBtn {
position: fixed;
right: 10rpx;
bottom: 20%;
padding: 20rpx;
z-index: 9999;
background: #f56c6c;
color: #fff;
border-radius: 50%;
}
</style>
更多推荐


所有评论(0)