我的场景:
uniapp上有一个A.vue页面,在A页面中引用了webview(html地址: ../../../hybrid/html/promotionPosterEdit/index.html),然后在html页面中使用了fabricjs绘制canvas,绘制完成后,在安卓app上打开这个weview,canvas.toDataurl()是可以使用的,但是当我移动canvas中的元素时,再次使用canvas.toDataUrl()就会报错:Failed to execute ‘toDataURL’ on ‘HTMLCanvasElement’: Tainted canvases may not be exported,
index.html部分代码片段:
console.log('保存本地', canvas) if(document.getElementsByClassName('canvasimg')[0]) { document.getElementsByClassName('canvasimg')[0].remove() } let dom = document.getElementById('canvas') var image = dom.toDataURL('image/png') var img = new Image(); img.src = image; img.crossOrigin = "anonymous"; img.setAttribute('style', 'position: absolute; top: 0px; left: -666px;width: 320px; height: 750px;') img.setAttribute('class', 'canvasimg') document.body.appendChild(img);
// 设置画布背景 fabric.Image.fromURL('../../../static/10801920.png', (img) => { img.set({ // 通过scale来设置图片大小,这里设置和画布一样大 scaleX: canvas.width / img.width, scaleY: canvas.height / img.height, }); // 设置背景 canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); canvas.renderAll(); }, {crossOrigin: 'anonymous'}); // 通过图片路径添加元素 fabric.Image.fromURL('../../../static/10801920.png', (img) => { img.set({ scaleX: canvas.width / img.width / 2, scaleY: canvas.height / img.height / 2, hasControls: true, // 是否开启图层的控件 }); canvas.add(img); }, {crossOrigin: 'anonymous'});
怎么样才能在操作元素后依旧能使用canvas.toDataUrl()不报错呢,我猜测可能是因为元素移动,canvas重绘后导致元素跨域的问题?同学们给给建议,感谢。
能在安卓app上使用动态添加文字、并且文字能够拖拽、编辑、删除、缩放、设置颜色、字体大小、文字排列方式,还有可以插入图片、并且图片也能缩放、删除。
假设场景:用户在后台页面上配置自定义海报,需要将数据传到app并展示。
我使用的是fabricjs。官网:http://fabricjs.com/
实现的原理就是使用webview间接实现,我在网上找了很久也没有找到比较快的方法实现,其实有一种,那就是使用canvas去实现,但是canvas的绘制很复杂、麻烦。大佬可以试试emmm
直接上代码了emmm,做个笔记。
index.vue的代码如下:
使用的插件image-tools:https://ext.dcloud.net.cn/plugin?id=123
<template> <view class="promotionPosterEdit"> <web-view :src="'../../../hybrid/html/promotionPosterEdit/index.html?data=' + obj" @message="getMessage" ref="wv"></web-view> </view> </template> <script> import { pathToBase64, base64ToPath } from '../../../js_sdk/mmmm-image-tools/index.js'; export default { data() { return { obj: null, currentWebview: null, } }, onLoad() { let statusBarHeight = uni.getSystemInfoSync()['statusBarHeight'] let top = uni.getStorageSync('navbarHeight') + 30 let height = uni.getSystemInfoSync()['screenHeight'] - 64 - uni.getStorageSync('navbarHeight') - 74 let width = uni.getSystemInfoSync()['screenWidth'] - 100 let screenHeight = uni.getSystemInfoSync()['screenHeight'] let obj = { statusBarHeight: statusBarHeight, top: top, height: height, width: width, screenHeight: screenHeight } this.obj = JSON.stringify(obj) //这里是传给webview的参数用来调整样式的 }, onReady() { const self = this self.currentWebview = self.$scope.$getAppWebview().children()[0] }, methods: { getMessage(e) { console.log('接收html发送的数据', e.detail) if(e.detail.data[0].msg === 'openCamera') { //打开手机相册 uni.chooseImage({ count: 1, success: (res) => { console.log('临时路径', res.tempFilePaths[0]) pathToBase64(res.tempFilePaths[0]).then(base64 => { let info = base64.replace(/[\r\n]/g, '') const self = this; self.currentWebview.evalJS(`uniEvent(${JSON.stringify(info)})`); //app向html发送数据 }).catch(error => { console.log('转换失败:', error); }); } }) } else if(e.detail.data[0].msg === 'save') { //保存到手机相册 console.log('app', e.detail.data[0].canvas) uni.showLoading({ title: '保存中...' }) let imageStr = e.detail.data[0].canvas base64ToPath(imageStr).then(path => { console.log('转换下载图片', path) this.saveImage(path); }).catch(error => { console.error('临时路径转换出错了:', error); }); } else if(e.detail.data[0].msg === 'wx') { //转发到微信 uni.showToast({ icon: 'none', title: '暂未开放' }) } else if(e.detail.data[0].msg === 'friend') { //转发到微信朋友圈 uni.showToast({ icon: 'none', title: '暂未开放' }) } else if(e.detail.data[0].msg === 'addEmptyText') { //不能添加空文字 uni.showToast({ icon: 'none', title: '请输入文字' }) } }, // 保存canvas图片到手机相册 saveImage(filePath) { uni.saveImageToPhotosAlbum({ filePath, // 需要临时文件路径,base64无法保存 success: () => { uni.hideLoading() uni.showToast({ icon: 'none', title: '保存成功' }) }, fail: (error) => { console.error('保存失败,请重试', error); } }); }, } } </script> <style lang="scss" scoped> .promotionPosterEdit { min-height: 100vh; background-color: #303030; } </style>
/hybrid/html/promotionPosterEdit/index.html的代码如下:
使用的插件html2canvas 1.4.1 https://html2canvas.hertzen.com
fabricjs5.3.0
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title>网络网页</title>
<link rel="stylesheet" href="./css/index.css" rel="external nofollow" />
</head>
<body>
<div class="post-message-section">
<div class="headers">
<div class="header">
<div class="left item" data-action="navigateBack">
<div class="back">
<img src="../img/back.png" alt="back" />
</div>
</div>
<div class="title item" style="color: #fff">
<div class="txt">宣传海报编辑</div>
</div>
<div class="right item">
<div class="share">
<img src="../img/share.png" alt="back" />
</div>
</div>
</div>
</div>
<div class="myCanvas">
<canvas id="canvas"></canvas>
</div>
<div class="shares">
<div class="sharesmask"></div>
<div class="content">
<div class="itemsurl">
<div class="item wx">
<div class="imgs">
<img src="../img/wechat.png" alt="wechat" srcset="" />
</div>
<div class="txt">微信</div>
</div>
<div class="item friends">
<div class="imgs">
<img src="../img/friends.png" alt="friends" srcset="" />
</div>
<div class="txt">朋友圈</div>
</div>
<!-- <div class="item">
<div class="imgs">
<img src="../../../static/download.png" alt="download" srcset="" />
</div>
<div class="txt">生成海报</div>
</div> -->
</div>
<div style="height: 6px;width: 100%;background-color: #ececec;"></div>
<div class="cancel">取消</div>
</div>
</div>
<div class="addText">
<div class="addTextmask"></div>
<div class="addTextContent">
<div class="title">文字样式设置</div>
<div class="tabtool">
<div class="items">
<input class="color" type="color" value="#000000" />
</div>
<div class="item bold">
<img src="../img/bold.png" alt="bold" srcset="" />
</div>
<div class="item italic">
<img src="../img/italic.png" alt="italic" srcset="" />
</div>
<div class="item amplify">
<img src="../img/amplify.png" alt="amplify" srcset="" />
</div>
<div class="item reduce">
<img src="../img/reduce.png" alt="reduce" srcset="" />
</div>
</div>
<div class="text">
<textarea id="MainText" cols="30" row="10" style="font-size: 18px;" placeholder="请输入文字"></textarea>
</div>
<div class="confirm">
<button class="cancelAddText btn btn-red" type="button">取消</button>
<button class="confirmAddText btn" type="button">确认</button>
</div>
</div>
</div>
<div class="footer">
<div class="itemBox">
<div class="item reload">
<div class="icons">
<img src="../img/reload.png" alt="reload" />
</div>
<div class="txt">恢复默认</div>
</div>
<div class="item pic">
<div class="icons">
<img src="../img/pic.png" alt="pic" />
</div>
<div class="txt">图片/二维码</div>
</div>
<div class="item edit-pen">
<div class="icons">
<img src="../img/edit-pen.png" alt="edit-pen" />
</div>
<div class="txt">添加文字</div>
</div>
<div class="item downs">
<div class="icons">
<img src="../img/downs.png" alt="downs" />
</div>
<div class="txt">保存本地</div>
</div>
</div>
</div>
<div class="deleteFooter">
<div class="delete">
<div class="box">
<img src="../img/delete.png" alt="delete" srcset="" />
<div class="tip">拖到此处删除</div>
</div>
</div>
<div class="deleteactive">
<div class="box">
<img src="../img/deleteactive.png" alt="deleteactive" srcset="" />
<div class="tip">松手即可删除</div>
</div>
</div>
</div>
<input class="imageinput" style="display: none;" type="image" accept="image/jpeg,image/jpg,image/png" capture />
</div>
<script src="../js/fabric.js"></script>
<script src="../js/html2canvas.min.js"></script>
<script type="text/javascript">
var userAgent = navigator.userAgent;
if (/quickapp/i.test(userAgent)) {
// quickapp
document.write('<script type="text/javascript" src="https://quickapp/jssdk.webview.min.js"><\/script>');
}
if (!/toutiaomicroapp/i.test(userAgent)) {
document.querySelector('.post-message-section').style.visibility = 'visible';
}
</script>
<!-- uni 的 SDK -->
<script src="../js/uni-webview.js"></script>
<!-- <script type="text/javascript" src="https://unpkg.com/@dcloudio/uni-webview-js@0.0.1/index.js"></script> -->
<script type="text/javascript">
// console.log('接收参数', decodeURIComponent(location.href.split('data=')[1]))
let params = JSON.parse(decodeURIComponent(location.href.split('data=')[1]))
document.getElementsByClassName('headers')[0].style.paddingTop = params.statusBarHeight + 'px'
document.getElementsByClassName('myCanvas')[0].style.paddingTop = params.top + 30 + 'px'
document.getElementById('canvas').setAttribute('width', params.width + 'px')
document.getElementById('canvas').setAttribute('height', params.height + 'px')
document.getElementsByClassName('post-message-section')[0].style.height = params.screenHeight + 'px'
// 生成canvas
var canvas = new fabric.Canvas('canvas');
// ...这里可以写canvas对象的一些配置,后面将会介绍
// console.log('fabric.Textbox', fabric.Textbox)
// 如果<canvas>标签没设置宽高,可以通过js动态设置
// canvas.setWidth(350)
// canvas.setHeight(200)
function initCanvas() {
// 读取图片地址,设置画布背景
fabric.Image.fromURL('../img/10801920.png', (img) => {
img.set({
// 通过scale来设置图片大小,这里设置和画布一样大
scaleX: canvas.width / img.width,
scaleY: canvas.height / img.height,
});
// 设置背景
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
canvas.renderAll();
}, {
crossOrigin: 'anonymous' //加上这个是因为我需要将海报下载到手机相册,允许跨域,如果没设置是下载不了图片的
});
// 通过图片路径添加
fabric.Image.fromURL('../img/10801920.png', (img) => {
img.set({
scaleX: canvas.width / img.width / 2,
scaleY: canvas.height / img.height / 2,
hasControls: true, // 是否开启图层的控件
});
// 添加对象后, 如下图
canvas.add(img);
// deleteDiv(img)
}, {
crossOrigin: 'anonymous'
});
// IText不可让文字竖排。textbox可以让文字竖排,自动根据宽度填写文字
const IText = new fabric.Textbox('这是一段初始默认文字', {
left: 50,
top: 50,
// width: 50, //不设置宽度,拉伸自动换行
fontSize: 18, // 字体大小
fontWeight: 600, // 字体粗细
fill: '#aaaaff', // 字体颜色
fontStyle: 'italic', // 斜体
splitByGrapheme: true, //true文本会实时根据宽度进行换行
hasControls: true, //元素操作控件显示\即描边正方体
// fontFamily: 'Delicious', // 设置字体
// fontFamily: 'monospace', // 设置字体
// fontFamily: 'fantasy', // 设置字体
// stroke: 'green', // 描边颜色
// strokeWidth: 3, // 描边宽度
// borderColor: 'orange',
// editingBorderColor: 'orange' // 点击文字进入编辑状态时的边框颜色
}, {
crossOrigin: 'anonymous'
});
// 添加文字
canvas.add(IText)
// deleteDiv(IText)
}
initCanvas()
// 删除元素操作
function deleteDiv(target) {
target['on']('moving', function(info) {
document.getElementsByClassName('deleteFooter')[0].style.display = 'block'
if(info.pointer.y > params.height) {
document.getElementsByClassName('delete')[0].style.display = 'none'
document.getElementsByClassName('deleteactive')[0].style.display = 'block'
} else {
document.getElementsByClassName('delete')[0].style.display = 'block'
document.getElementsByClassName('deleteactive')[0].style.display = 'none'
}
})
target['on']('mouseup', function(info) {
if(info.pointer.y > params.height + 5) {
canvas.remove(target)
}
document.getElementsByClassName('delete')[0].style.display = 'block'
document.getElementsByClassName('deleteactive')[0].style.display = 'none'
document.getElementsByClassName('deleteFooter')[0].style.display = 'none'
})
}
// webview页面加载完成
document.addEventListener('UniAppJSBridgeReady', function() {
// webview加载完成,显示页面
document.getElementsByClassName('post-message-section')[0].style.display = 'block'
// 返回上一页
document.querySelector('.left').addEventListener('click', function(evt) {
uni.navigateBack()
});
// 分享
document.querySelector('.right').addEventListener('click', function(evt) {
console.log(getComputedStyle(document.documentElement).getPropertyValue("--sat"))
console.log(getComputedStyle(document.documentElement).getPropertyValue("--sab"))
document.querySelector('.shares').style.display = 'block'
slideTimer(0, 'add', 1, 30)
});
document.querySelector('.sharesmask').addEventListener('click', function(evt) {
console.log('点击分享遮罩层', evt.target)
slideTimer(200, 'dec', 1, 30)
})
// 取消分享
document.querySelector('.cancel').addEventListener('click', function(evt) {
slideTimer(200, 'dec', 1, 30)
})
// 高度动画
function slideTimer(height, type, setup, times) {
let timer = null
let heights = height
let setups = setup
timer = setInterval(() => {
if(type === 'add') {
heights += 10 * setups
} else {
heights -= 10 * setups
}
console.log(Number(document.querySelector('.content').style.height.replace('px', '')))
document.querySelector('.content').style.height = heights + 'px'
setups++
if (Number(heights) <= 0 || Number(document.querySelector('.content').style.height.replace('px', '')) >= 200) {
clearInterval(timer)
if(type !== 'add') {
document.querySelector('.shares').style.display = 'none'
}
heights = 0
setups = 1
}
}, times)
}
// 分享到微信
document.querySelector('.wx').addEventListener('click', function(evt) {
console.log('分享到微信')
uni.postMessage({
data: {
msg: 'wx'
}
});
})
// 分享到朋友圈
document.querySelector('.friends').addEventListener('click', function(evt) {
console.log('分享到朋友圈')
uni.postMessage({
data: {
msg: 'friend'
}
});
})
// 点击添加文字
document.querySelector('.edit-pen').addEventListener('click', function(evt) {
console.log('添加文字')
document.querySelector('.addText').style.display = 'block'
initAddText()
document.getElementsByClassName('color')[0].onchange = function(evt) {
console.log('颜色改变', evt.target.value)
document.getElementById('MainText').style.color = evt.target.value
}
})
// 点击加粗文字
document.querySelector('.bold').addEventListener('click', function(evt) {
console.log('点击加粗文字')
if(document.getElementById('MainText').style.fontWeight === 'bold') {
document.getElementById('MainText').style.fontWeight = 'normal'
} else {
document.getElementById('MainText').style.fontWeight = 'bold'
}
})
// 点击斜体文字
document.querySelector('.italic').addEventListener('click', function(evt) {
console.log('点击斜体文字')
if(document.getElementById('MainText').style.fontStyle === 'italic') {
document.getElementById('MainText').style.fontStyle = 'normal'
} else {
document.getElementById('MainText').style.fontStyle = 'italic'
}
})
// 点击文字变大
document.querySelector('.amplify').addEventListener('click', function(evt) {
console.log('点击文字变大')
console.log(Number(document.getElementById('MainText').style.fontSize.replace('px', '')))
console.log(document.getElementById('MainText').style.fontSize.replace('px', ''))
document.getElementById('MainText').style.fontSize = Number(document.getElementById('MainText').style.fontSize.replace('px', ''))+1 + 'px'
})
// 点击文字变小
document.querySelector('.reduce').addEventListener('click', function(evt) {
console.log('点击文字变小')
if(Number(document.getElementById('MainText').style.fontSize.replace('px', '')) === 12) {
console.log('无法再减了')
} else {
document.getElementById('MainText').style.fontSize = Number(document.getElementById('MainText').style.fontSize.replace('px', ''))-1 + 'px'
}
})
// 确认添加文字到canva中
document.getElementsByClassName('confirmAddText')[0].addEventListener('click', function(evt) {
console.log('确认添加到canva中')
if(document.getElementById('MainText').value !== '') {
// IText不可让文字竖排。textbox可以让文字竖排,自动根据宽度填写文字
const IText = new fabric.Textbox(document.getElementById('MainText').value, {
left: 50,
top: 50,
// width: 50, //不设置宽度,拉伸自动换行
fontSize: Number(document.getElementById('MainText').style.fontSize.replace('px', '')), // 字体大小
fontWeight: document.getElementById('MainText').style.fontWeight, // 字体粗细
fill: document.getElementById('MainText').style.color, // 字体颜色
fontStyle: document.getElementById('MainText').style.fontStyle, // 斜体
splitByGrapheme: true, //true文本会实时根据宽度进行换行
hasControls: true, //元素操作控件显示\即描边正方体
}, {
crossOrigin: 'anonymous'
});
// 添加文字
canvas.add(IText);
deleteDiv(IText)
initAddText()
document.querySelector('.addText').style.display = 'none'
} else {
uni.postMessage({
data: {
msg: 'addEmptyText'
}
});
}
})
// 关闭添加文字
document.querySelector('.addTextmask').addEventListener('click', function(evt) {
initAddText()
document.querySelector('.addText').style.display = 'none'
})
// 取消添加文字
document.getElementsByClassName('cancelAddText')[0].addEventListener('click', function(evt) {
console.log('取消添加文字')
initAddText()
document.querySelector('.addText').style.display = 'none'
})
// 初始化添加文字
function initAddText() {
document.getElementById('MainText').style.fontWeight = 'normal'
document.getElementById('MainText').style.fontStyle = 'normal'
document.getElementById('MainText').style.fontSize = '18px'
document.getElementById('MainText').style.color = '#000000'
document.getElementById('MainText').value = ''
document.getElementsByClassName('color')[0].value = '#000000'
}
// 恢复默认canvas
document.querySelector('.reload').addEventListener('click', function(evt) {
console.log('恢复默认')
canvas.clear();
initCanvas()
})
// app主动与html通信,用于添加图片到canvas中
function addUniEvenPassthrough() {
window.uniEvent = function(info) {
console.log('获取完相册了', info)
fabric.Image.fromURL(info, (img) => {
img.set({
scaleX: canvas.width / img.width / 2,
scaleY: canvas.height / img.height / 2,
hasControls: true, // 是否开启图层的控件
});
// 添加对象后, 如下图
canvas.add(img);
deleteDiv(img)
}, {
crossOrigin: 'anonymous'
});
}
}
addUniEvenPassthrough()
// 点击了图片/二维码
document.querySelector('.pic').addEventListener('click', function(evt) {
console.log('点击了图片/二维码')
uni.postMessage({
data: {
msg: 'openCamera'
}
});
})
// 海报保存本地
document.querySelector('.downs').addEventListener('click', function(evt) {
console.log('保存本地')
if (document.getElementsByClassName('canvasimg')[0]) {
document.getElementsByClassName('canvasimg')[0].remove()
}
let dom = document.getElementById('canvas')
var img = new Image();
img.crossOrigin = "anonymous"; //关键、允许跨域
var image = dom.toDataURL('image/png')
img.src = image;
img.setAttribute('style','position: absolute; top: 0px; left: -666px;width: 320px; height: 750px;')
img.setAttribute('class', 'canvasimg')
document.body.appendChild(img);
let canvasdom = document.getElementsByTagName('canvas')[0]
html2canvas(canvasdom, {
width: canvasdom.clientWidth, //dom 原始宽度
height: canvasdom.clientHeight,
scrollY: 0, // html2canvas默认绘制视图内的页面,需要把scrollY,scrollX设置为0
scrollX: 0,
useCORS: true, //支持跨域
scale: 2, // 设置生成图片的像素比例,默认是1,如果生成的图片模糊的话可以开启该配置项。(设置为1无法触发长按扫一扫)
}).then((res) => {
console.log(res)
uni.postMessage({
data: {
msg: 'save',
canvas: res.toDataURL('image/png').replace(/[\r\n]/g, '')
}
});
}).catch(err => {
console.log('生成失败')
})
})
});
</script>
</body>
</html>