web-base-h5/components/architectures/Poster.vue

529 lines
18 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="canvas">
<view ref="Content" class="theContent">
<slot></slot>
</view>
<canvas canvas-id="myCanvas" id="myCanvas" @onReady="onCanvasReady"
:style="{ width: width + 'px', height: height + 'px' }"></canvas>
</view>
</template>
<!--
list参数说明
图片渲染
type: 'image',
x: X轴位置,
y: Y轴位置,
path: 图片路径,
width: 图片宽度,
height: 图片高度,
rotate: 旋转角度
mode:'center' 居中自适应
shape: 形状默认无可选值circle 圆形 如果是number,则生成圆角矩形
area: {x,y,width,height} // 绘制范围,超出该范围会被剪裁掉 该属性与shape暂时无法同时使用area存在时shape失效
矩形渲染
type: 'square',
x: X轴位置,
y: Y轴位置,
width: 图片宽度,
height: 图片高度,
rotate: 旋转角度
shape: 形状默认无可选值circle 圆形 如果是number,则生成圆角矩形
fillStyle:填充的背景样式
area: {x,y,width,height} // 绘制范围,超出该范围会被剪裁掉 该属性与shape暂时无法同时使用area存在时shape失效
文字渲染
type: 'text',
x: X轴位置,
y: Y轴位置,
text: 文本内容,
size: 字体大小,
textBaseline 基线 默认top 可选值'top''bottom''middle''normal'
color: 颜色
多行文字渲染
type: 'textarea',
x: X轴位置,
y: Y轴位置,
width换行的宽度
height: 高度溢出会展示...
lineSpace: 行间距
text: 文本内容,
size: 字体大小,
textBaseline 基线 默认top 可选值'top''bottom''middle''normal'
color: 颜色
-->
<script>
let timer = null;
export default {
name: "Poster",
props: {
// 绘制队列
list: {
type: Array,
required: true,
},
// 海报宽度(默认设备宽度放大两倍) 建议都放大两倍
width: {
type: [Number, String],
default: uni.getSystemInfoSync().windowWidth * 2,
},
// 海报高度(默认设备高度放大两倍)
height: {
type: [Number, String],
default: uni.getSystemInfoSync().windowHeight * 2,
},
//背景颜色
backgroundColor: {
type: String,
default: "rgba(0,0,0,0)",
},
},
data() {
return {
posterUrl: "",
renderList: [],
ctx: null, //画布上下文
counter: -1, //计数器
drawPathQueue: [], //画图路径队列
};
},
watch: {
drawPathQueue(newVal, oldVal) {
// 绘制单行文字
const fillText = (textOptions) => {
this.ctx.setFillStyle(textOptions.color);
this.ctx.setFontSize(textOptions.size);
this.ctx.setTextBaseline(textOptions.textBaseline || "top");
this.ctx.fillText(textOptions.text, textOptions.x, textOptions.y);
};
// 绘制段落
const fillParagraph = (textOptions) => {
this.ctx.setFontSize(textOptions.size);
let tempOptions = JSON.parse(JSON.stringify(textOptions));
// 如果没有指定行间距则设置默认值
tempOptions.lineSpace = tempOptions.lineSpace ? tempOptions.lineSpace : 10;
// 获取字符串
let str = textOptions.text;
// 计算指定高度可以输出的最大行数
let lineCount = Math.floor((tempOptions.height + tempOptions.lineSpace) / (tempOptions.size +
tempOptions.lineSpace));
// 初始化单行宽度
let lineWidth = 0;
let lastSubStrIndex = 0; //每次开始截取的字符串的索引
// 构建一个打印数组
let strArr = str.split("");
let drawArr = [];
let text = "";
while (strArr.length) {
let word = strArr.shift();
text += word;
let textWidth = this.ctx.measureText(text).width;
if (textWidth > textOptions.width) {
// 因为超出宽度 所以要截取掉最后一个字符
text = text.substr(0, text.length - 1);
drawArr.push(text);
text = "";
// 最后一个字还给strArr
strArr.unshift(word);
} else if (!strArr.length) {
drawArr.push(text);
}
}
if (drawArr.length > lineCount) {
// 超出最大行数
drawArr.length = lineCount;
let pointWidth = this.ctx.measureText("...").width;
let wordWidth = 0;
let wordArr = drawArr[drawArr.length - 1].split("");
let words = "";
while (pointWidth > wordWidth) {
words += wordArr.pop();
wordWidth = this.ctx.measureText(words).width;
}
drawArr[drawArr.length - 1] = wordArr.join("") + "...";
}
// 打印
for (let i = 0; i < drawArr.length; i++) {
let _h = i > 0 ? tempOptions.size + tempOptions.lineSpace : 0;
tempOptions.y = tempOptions.y + _h; // y的位置
tempOptions.text = drawArr[i]; // 绘制的文本
fillText(tempOptions);
}
};
const roundRect = (x, y, w, h, r) => {
// 开始绘制
this.ctx.beginPath();
// 因为边缘描边存在锯齿,最好指定使用 transparent 填充
// 这里是使用 fill 还是 stroke都可以二选一即可
this.ctx.setFillStyle("transparent");
// ctx.setStrokeStyle('transparent')
// 左上角
this.ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5);
// border-top
this.ctx.moveTo(x + r, y);
this.ctx.lineTo(x + w - r, y);
this.ctx.lineTo(x + w, y + r);
// 右上角
this.ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2);
// border-right
this.ctx.lineTo(x + w, y + h - r);
this.ctx.lineTo(x + w - r, y + h);
// 右下角
this.ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5);
// border-bottom
this.ctx.lineTo(x + r, y + h);
this.ctx.lineTo(x, y + h - r);
// 左下角
this.ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI);
// border-left
this.ctx.lineTo(x, y + r);
this.ctx.lineTo(x + r, y);
// 这里是使用 fill 还是 stroke都可以二选一即可但是需要与上面对应
this.ctx.fill();
// ctx.stroke()
this.ctx.closePath();
// 剪切
this.ctx.clip();
};
// 图片自适应
const adaptiveImg = (img, x, y, mode) => {
const {
imgW: w,
imgH: h,
width: dWidth,
height: dHeight
} = img;
let dw = dWidth / w; //canvas与图片的宽比
let dh = dHeight / h; //canvas与图片的高比
// 裁剪图片中间部分
if ((w > dWidth && h > dHeight) || (w < dWidth && h < dHeight)) {
if (dw > dh) {
this.ctx.drawImage(img.path, 0, (h - dHeight / dw) / 2, w, dHeight / dw, x, y, dWidth,
dHeight);
} else {
this.ctx.drawImage(img.path, (w - dWidth / dh) / 2, 0, dWidth / dh, h, x, y, dWidth,
dHeight);
}
} else {
// 拉伸图片
if (w < dWidth) {
this.ctx.drawImage(img.path, 0, (h - dHeight / dw) / 2, w, dHeight / dw, x, y, dWidth,
dHeight);
} else {
this.ctx.drawImage(img.path, (w - dWidth / dh) / 2, 0, dWidth / dh, h, x, y, dWidth,
dHeight);
}
}
// 裁剪图片中间部分
// this.ctx.drawImage(img.path, sx, sy, sWidth, sHeight, x, y, dWidth, dHeight);
// this.ctx.drawImage(img.path, x, y, temp.dWidth, temp.dHeight);
};
// 绘制背景
this.ctx.setFillStyle(this.backgroundColor);
this.ctx.fillRect(0, 0, this.width, this.height);
/* 所有元素入队则开始绘制 */
if (newVal.length === this.renderList.length) {
if (newVal.length == 0) {
this.$emit("on-error", {
msg: "数据为空",
});
return;
}
try {
// console.log('生成的队列:' + JSON.stringify(newVal));
console.log("开始绘制...");
for (let i = 0; i < this.drawPathQueue.length; i++) {
for (let j = 0; j < this.drawPathQueue.length; j++) {
let current = this.drawPathQueue[j];
/* 按顺序绘制 */
if (current.index === i) {
/* 文本绘制 */
if (current.type === "text") {
console.log("开始绘制...text");
fillText(current);
this.counter--;
}
/* 多行文本 */
if (current.type === "textarea") {
console.log("开始绘制...textarea");
fillParagraph(current);
this.counter--;
}
/* 多行文本 */
if (current.type === "square") {
console.log("开始绘制...square");
this.ctx.save(); // 保存上下文,绘制后恢复
this.ctx.beginPath(); //开始绘制
//画好了圆 剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 这也是我们要save上下文的原因
// 设置旋转中心
let offsetX = current.x + Number(current.width) / 2;
let offsetY = current.y + Number(current.height) / 2;
this.ctx.translate(offsetX, offsetY);
let degrees = current.rotate ? Number(current.rotate) % 360 : 0;
this.ctx.rotate((degrees * Math.PI) / 180);
this.ctx.fillStyle = current.fillStyle;
this.ctx.fillRect(current.x - offsetX, current.y - offsetY, current.width, current
.height);
this.ctx.closePath();
this.ctx.restore(); // 恢复之前保存的上下文
this.counter--;
}
/* 图片绘制 */
if (current.type === "image") {
console.log("开始绘制...image");
if (current.area) {
// 绘制绘图区域
this.ctx.save();
this.ctx.beginPath(); //开始绘制
this.ctx.rect(current.area.x, current.area.y, current.area.width, current.area
.height);
this.ctx.clip();
// 设置旋转中心
let offsetX = current.x + Number(current.width) / 2;
let offsetY = current.y + Number(current.height) / 2;
this.ctx.translate(offsetX, offsetY);
let degrees = current.rotate ? Number(current.rotate) % 360 : 0;
this.ctx.rotate((degrees * Math.PI) / 180);
this.ctx.drawImage(current.path, current.x - offsetX, current.y - offsetY,
current.width, current.height);
this.ctx.closePath();
this.ctx.restore(); // 恢复之前保存的上下文
} else if (current.shape == "circle") {
this.ctx.save(); // 保存上下文,绘制后恢复
this.ctx.beginPath(); //开始绘制
//先画个圆 前两个参数确定了圆心 x,y 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false即顺时针
let width = current.width / 2 + current.x;
let height = current.height / 2 + current.y;
let r = current.width / 2;
this.ctx.arc(width, height, r, 0, Math.PI * 2);
this.ctx.lineTo(current.x, current.y);
this.ctx.fill();
//画好了圆 剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 这也是我们要save上下文的原因
this.ctx.clip();
// 设置旋转中心
let offsetX = current.x + Number(current.width) / 2;
let offsetY = current.y + Number(current.height) / 2;
this.ctx.translate(offsetX, offsetY);
let degrees = current.rotate ? Number(current.rotate) % 360 : 0;
this.ctx.rotate((degrees * Math.PI) / 180);
current.mode ?
adaptiveImg(current, current.x - offsetX, current.y - offsetY, current
.mode) :
this.ctx.drawImage(current.path, current.x - offsetX, current.y - offsetY,
current.width, current.height);
this.ctx.closePath();
this.ctx.restore(); // 恢复之前保存的上下文
} else if (typeof current.shape == "number") {
this.ctx.save(); // 保存上下文,绘制后恢复
this.ctx.beginPath(); //开始绘制
roundRect(current.x, current.y, current.width, current.height, current.shape);
//画好了圆 剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 这也是我们要save上下文的原因
// 设置旋转中心
let offsetX = current.x + Number(current.width) / 2;
let offsetY = current.y + Number(current.height) / 2;
this.ctx.translate(offsetX, offsetY);
let degrees = current.rotate ? Number(current.rotate) % 360 : 0;
this.ctx.rotate((degrees * Math.PI) / 180);
current.mode ?
adaptiveImg(current, current.x - offsetX, current.y - offsetY, current
.mode) :
this.ctx.drawImage(current.path, current.x - offsetX, current.y - offsetY,
current.width, current.height);
this.ctx.closePath();
this.ctx.restore(); // 恢复之前保存的上下文
} else {
this.ctx.drawImage(current.path, current.x, current.y, current.width, current
.height);
}
this.counter--;
}
}
}
}
} catch (err) {
console.log(err);
this.$emit("on-error", err);
}
}
},
counter(newVal, oldVal) {
if (newVal === 0) {
this.ctx.draw(false, (draw) => {
uni.canvasToTempFilePath({
canvasId: "myCanvas",
success: (res) => {
// 在H5平台下tempFilePath 为 base64
console.log("图片已保存至本地:", res.tempFilePath);
this.posterUrl = res.tempFilePath;
this.$emit("on-success", res.tempFilePath);
},
fail: (error) => {
this.$emit("on-error", error);
},
},
this
);
});
}
},
},
mounted() {
// this.generateImg();
// console.log('mounted');
},
methods: {
onCanvasReady() {
// #ifdef MP-ALIPAY
const query = my.createSelectorQuery();
query
.select("#myCanvas")
.node()
.exec((res) => {
const canvas = res[0].node;
const ctx = canvas.getContext("2d");
});
// #endif
},
/**
* @param {*} elClass 元素名称
* @param {*} slot 是否采用slot方式
* @param {*} startX X偏移
* @param {*} startY Y偏移
* @return {*}
*/
createForElRect(elClass = "Poster", slot = true, startX = 0, startY = 0) {
uni
.createSelectorQuery()
.selectAll("." + elClass)
.fields({
dataset: true,
size: true,
rect: true,
value: true,
scrollOffset: true,
properties: ["src", "mode"],
computedStyle: ["margin", "padding", "backgroundColor", "fontSize", "color", "fontWeight",
"borderRadius"
],
context: true,
},
(res) => {
let list = [];
const sys = uni.getSystemInfoSync();
let multiple = sys.windowWidth / this.width;
res.forEach((val, index) => {
let src = val.src || val.dataset.enode || "";
let type = val.src ? "image" : val.dataset.etype || "text";
let text = val.dataset.enode || "";
let size = val.fontSize.replace("px", "") || 13;
let shape = val.borderRadius == "50%" ? "circle" : val.borderRadius.replace("px",
"") * 2;
let x = (startX + val.left - (slot ? sys.screenWidth : 0)) / multiple;
let y = (startY + val.top) / multiple;
y = (startY + val.top - (slot ? 50 : 0)) / multiple;
// #ifdef H5
y = (startY + val.top) / multiple;
// #endif
list.push({
type: type,
shape,
text,
mode: "center",
x,
y,
path: src,
width: val.width / multiple,
height: val.height / multiple,
size: size / multiple,
color: val.color,
});
});
let canvas = uni.createCanvasContext("myCanvas", this);
this.ctx = canvas;
this.renderList = [...this.list, ...list];
this.generateImg();
}
)
.exec();
},
create() {
let canvas = uni.createCanvasContext("myCanvas", this);
this.ctx = canvas;
this.renderList = this.list;
this.generateImg();
},
async generateImg() {
console.log("generateimg");
this.counter = this.renderList.length;
this.drawPathQueue = [];
const getImgInfo = async (current) => {
// console.log("current", current);
return new Promise((resolve, reject) => {
uni.getImageInfo ?
uni.getImageInfo({
src: current.path,
success: (res) => {
current.path = res.path;
current.imgW = res.width;
current.imgH = res.height;
// this.drawPathQueue.push(current);
resolve(current);
},
}) :
resolve(current);
});
};
const delayedLog = async (v, i) => {
let current = this.renderList[i];
current.index = i;
/* 如果是文本直接放入队列 */
if (current.type === "text" || current.type === "textarea" || current.type === "square") {
// this.drawPathQueue.push(current);
return current;
} else {
return await getImgInfo(current);
/* 图片需获取本地缓存path放入队列 */
}
};
const processArray = async (array) => {
// map array to promises
const promises = array.map((v, i) => delayedLog(v, i));
// wait until all promises are resolved
const allData = await Promise.all(promises);
console.log("Done!", allData);
return allData;
};
this.drawPathQueue = await processArray(this.renderList);
},
saveImg() {
uni.canvasToTempFilePath({
canvasId: "myCanvas",
success: (res) => {
// 在H5平台下tempFilePath 为 base64
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
console.log("save success");
},
});
},
},
this
);
},
},
};
</script>
<style lang="scss" scoped>
.canvas {
position: fixed;
top: 100rpx;
left: 750rpx;
width: 100vw;
}
.theContent {}
</style>