1、创建海报的基本逻辑
- 1、先创建dom元素
wrapperHeight
是根据海报的内容计算出来海报的高度
<view class="preview-card-wrap" @tap.stop>
<view class="poster-box" :class="{ show: isDrew }">
<canvas
id="myCanvas"
canvas-id="myCanvas"
:style="{
width: '750rpx',
height: wrapperHeight + 'rpx',
}"
></canvas>
</view>
</view>
class Poster {
canvasId: string;
instanceComponent: ReturnType<typeof getCurrentInstance> | undefined;
ctx: UniApp.CanvasContext | undefined = undefined;
width = 0;
height = 0;
isPixel: boolean;
drawSteps: DrawSteps = [];
constructor(
canvasId: string,
instance?: ReturnType<typeof getCurrentInstance>,
{ isPixel = true } = <InitConfig>{}
) {
this.canvasId = canvasId;
this.instanceComponent = instance;
this.ctx = uni.createCanvasContext(canvasId, instance);
this.isPixel = isPixel;
}
public setCanvasSize(width: number, height: number) {
this.width = width;
this.height = height;
}
clearAll() {
this.ctx?.clearRect(
0,
0,
this.getPixel(this.width),
this.getPixel(this.height)
);
this.ctx?.draw();
}
}
export default Poster;
import { getCurrentInstance, ref } from "vue";
const poster = ref<Poster | undefined | any>(undefined);
poster.value = new Poster("myCanvas", self, { isPixel: false });
const self = getCurrentInstance();
poster.value.setCanvasSize(750, wrapperHeight.value);
poster.value.clearAll();
2、用canvas绘制文字
getPixel(size: number) {
return this.isPixel ? size : rpx2px(size);
}
public getTextWidth(text: string, fontStyle?: string) {
if (!this.ctx || !text.trim()) return 0;
this.ctx.save();
this.ctx.font = fontStyle || "14px sans-serif";
const dimension = this.ctx.measureText(text);
this.ctx.restore();
return this.isPixel ? dimension.width : px2rpx(dimension.width);
}
public correctEllipsisText(text: string, width: number, fontStyle?: string) {
let resultText = "";
const strSplits = text.split("");
while (strSplits.length > 0) {
const s = strSplits.shift();
const isGtWidth =
this.getPixel(this.getTextWidth(resultText + s, fontStyle)) >
this.getPixel(width);
if (isGtWidth) {
resultText = resultText.substring(0, resultText.length) + "...";
break;
}
resultText += s;
}
return resultText;
}
public async drawText({
text,
x,
y,
maxWidth,
color,
fontSize,
fontFamily,
fontWeight = 500,
borderWidth,
borderColor,
lineHeight = 1.2,
UseEllipsis = true,
}: TextDrawStep) {
if (!this.ctx) return;
const fontStyle = `${fontWeight} ${
fontSize ? this.getPixel(fontSize) : 14
}px ${`${fontFamily}` || "sans-serif"}`;
this.ctx.save();
this.ctx.setTextBaseline("top");
this.ctx.font = fontStyle;
color && (this.ctx.fillStyle = color);
if (borderColor) this.ctx.strokeStyle = borderColor;
if (borderWidth) {
this.ctx.lineWidth = borderWidth;
}
if (UseEllipsis) {
const drawText = this.correctEllipsisText(
text,
maxWidth || this.width,
fontStyle
);
if (borderWidth) {
this.ctx.strokeText(
drawText,
this.getPixel(x),
this.getPixel(y),
maxWidth ? this.getPixel(maxWidth) : maxWidth
);
}
this.ctx.fillText(
drawText,
this.getPixel(x),
this.getPixel(y),
maxWidth ? this.getPixel(maxWidth) : maxWidth
);
} else {
const words = text.split("");
let line = "";
let yPos = y;
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i];
const textWidth = this.getTextWidth(testLine, fontStyle);
if (textWidth > this.getPixel(maxWidth || this.width)) {
if (borderWidth) {
this.ctx.strokeText(
line,
this.getPixel(x),
this.getPixel(y),
maxWidth ? this.getPixel(maxWidth) : maxWidth
);
this.ctx.fillText(
line,
this.getPixel(x),
this.getPixel(yPos),
maxWidth ? this.getPixel(maxWidth) : maxWidth
);
yPos += lineHeight * (fontSize ? this.getPixel(fontSize) : 14);
} else {
line = testLine;
}
}
}
this.ctx.strokeText(line, this.getPixel(x), this.getPixel(y));
this.ctx.fillText(line, this.getPixel(x), this.getPixel(yPos));
}
this.ctx.restore();
}
3、绘制矩形
public drawLineShape({ lines, fillColor }: LineShapeDrawStep) {
if (!this.ctx || !lines.length) return;
this.ctx.save();
this.ctx.beginPath();
const [x, y] = lines[0];
this.ctx.moveTo(this.getPixel(x), this.getPixel(y));
for (let i = 1; i < lines.length; i++) {
const [ix, iy] = lines[i];
this.ctx.lineTo(this.getPixel(ix), this.getPixel(iy));
}
if (this.ctx && fillColor) {
this.ctx.fillStyle = fillColor;
this.ctx.fill();
} else {
this.ctx.closePath();
}
this.ctx.restore();
}
4、绘制圆形
public drawCircleShape({
x,
y,
radius,
startAngle,
endAngle,
anticlockwise,
fillColor,
}: CircleShapeDrawStep) {
if (!this.ctx) return;
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(
this.getPixel(x),
this.getPixel(y),
this.getPixel(radius),
this.getPixel(startAngle),
this.getPixel(endAngle),
anticlockwise
);
if (this.ctx && fillColor) {
this.ctx.setFillStyle(fillColor);
this.ctx.fill();
} else {
this.ctx.closePath();
}
this.ctx.restore();
}
5、绘制圆角矩形
public roundRect = ({
x,
y,
width,
height,
radius,
fillColor,
}: roundRectShapeDrawStep) => {
if (!this.ctx) return;
const dx = this.getPixel(x);
const dy = this.getPixel(y);
const dRadius = this.getPixel(radius);
const dWidth = this.getPixel(width);
const dHeight = this.getPixel(height);
this.ctx.beginPath();
this.ctx.moveTo(dx + dRadius, dy);
this.ctx.lineTo(dx + dWidth - dRadius, dy);
this.ctx.arcTo(dx + dWidth, dy, dx + dWidth, dy + dRadius, dRadius);
this.ctx.lineTo(dx + dWidth, dy + dHeight - dRadius);
this.ctx.arcTo(
dx + dWidth,
dy + dHeight,
dx + dWidth - dRadius,
dy + dHeight,
dRadius
);
this.ctx.lineTo(dx + dRadius, dy + dHeight);
this.ctx.arcTo(dx, dy + dHeight, dx, dy + dHeight - dRadius, dRadius);
this.ctx.lineTo(dx, dy + dRadius);
this.ctx.arcTo(dx, dy, dx + dRadius, dy, dRadius);
this.ctx.closePath();
this.ctx.fillStyle = fillColor;
this.ctx.fill();
};
6、绘制图片
public async drawImage({
image,
x,
y,
width,
height,
isCircle = false,
clipConfig,
}: ImageDrawStep) {
if (!this.ctx) return;
this.ctx.save();
if (isCircle) {
const r = Math.floor(this.getPixel(width) / 2);
this.ctx.arc(
this.getPixel(x) + r,
this.getPixel(y) + r,
r,
0,
2 * Math.PI
);
this.ctx.clip();
}
await sleep(50);
let clipParams: number[] = [];
if (clipConfig) {
clipParams = [
clipConfig.x,
clipConfig.y,
clipConfig.width,
clipConfig.height,
];
}
this.ctx.drawImage(
image,
...clipParams,
this.getPixel(x),
this.getPixel(y),
this.getPixel(width),
this.getPixel(height)
);
this.ctx.restore();
}
7、执行绘制
async draw(callback?: Function) {
let index = 0;
while (index < this.drawSteps.length) {
const { type, ...otherProps } = <DrawStep>this.drawSteps[index];
const stepProps = <AnyObject>otherProps;
const props = <AnyObject>{};
Object.assign(
props,
stepProps.getProps
? await stepProps.getProps(this.drawSteps, index)
: stepProps
);
if (type === DrawType.Text) {
await this.drawText(<TextDrawStep>props);
} else if (type === DrawType.Image) {
await this.drawImage(<ImageDrawStep>props);
} else if (type === DrawType.LineShape) {
await this.drawLineShape(<LineShapeDrawStep>props);
} else if (type === DrawType.CircleShape) {
await this.drawCircleShape(<CircleShapeDrawStep>props);
} else if (type === DrawType.RoundRectShape) {
await this.roundRect(<roundRectShapeDrawStep>props);
}
props.immediateDraw && (await this.syncDraw());
index += 1;
}
this.ctx?.draw(true, (res) => {
callback?.(res);
});
}
8、完整的代码
import type { getCurrentInstance } from "vue";
import { px2rpx, rpx2px } from "@/utils/view";
import type {
DrawStep,
DrawSteps,
ImageDrawStep,
InitConfig,
LineShapeDrawStep,
TextDrawStep,
CircleShapeDrawStep,
roundRectShapeDrawStep,
} from "./poster";
import { DrawType } from "./poster";
import { sleep } from "@/utils";
class Poster {
canvasId: string;
instanceComponent: ReturnType<typeof getCurrentInstance> | undefined;
ctx: UniApp.CanvasContext | undefined = undefined;
width = 0;
height = 0;
isPixel: boolean;
drawSteps: DrawSteps = [];
constructor(
canvasId: string,
instance?: ReturnType<typeof getCurrentInstance>,
{ isPixel = true } = <InitConfig>{}
) {
this.canvasId = canvasId;
this.instanceComponent = instance;
this.ctx = uni.createCanvasContext(canvasId, instance);
this.isPixel = isPixel;
}
getPixel(size: number) {
return this.isPixel ? size : rpx2px(size);
}
public getCanvasSize() {
return {
width: this.width,
height: this.height,
};
}
public setCanvasSize(width: number, height: number) {
this.width = width;
this.height = height;
}
public getTextWidth(text: string, fontStyle?: string) {
if (!this.ctx || !text.trim()) return 0;
this.ctx.save();
this.ctx.font = fontStyle || "14px sans-serif";
const dimension = this.ctx.measureText(text);
this.ctx.restore();
return this.isPixel ? dimension.width : px2rpx(dimension.width);
}
public correctEllipsisText(text: string, width: number, fontStyle?: string) {
let resultText = "";
const strSplits = text.split("");
while (strSplits.length > 0) {
const s = strSplits.shift();
const isGtWidth =
this.getPixel(this.getTextWidth(resultText + s, fontStyle)) >
this.getPixel(width);
if (isGtWidth) {
resultText = resultText.substring(0, resultText.length) + "...";
break;
}
resultText += s;
}
return resultText;
}
public async drawImage({
image,
x,
y,
width,
height,
isCircle = false,
clipConfig,
}: ImageDrawStep) {
if (!this.ctx) return;
this.ctx.save();
if (isCircle) {
const r = Math.floor(this.getPixel(width) / 2);
this.ctx.arc(
this.getPixel(x) + r,
this.getPixel(y) + r,
r,
0,
2 * Math.PI
);
this.ctx.clip();
}
await sleep(50);
let clipParams: number[] = [];
if (clipConfig) {
clipParams = [
clipConfig.x,
clipConfig.y,
clipConfig.width,
clipConfig.height,
];
}
this.ctx.drawImage(
image,
...clipParams,
this.getPixel(x),
this.getPixel(y),
this.getPixel(width),
this.getPixel(height)
);
this.ctx.restore();
}
public async drawText({
text,
x,
y,
maxWidth,
color,
fontSize,
fontFamily,
fontWeight = 500,
borderWidth,
borderColor,
lineHeight = 1.2,
UseEllipsis = true,
}: TextDrawStep) {
if (!this.ctx) return;
const fontStyle = `${fontWeight} ${
fontSize ? this.getPixel(fontSize) : 14
}px ${`${fontFamily}` || "sans-serif"}`;
this.ctx.save();
this.ctx.setTextBaseline("top");
this.ctx.font = fontStyle;
color && (this.ctx.fillStyle = color);
if (borderColor) this.ctx.strokeStyle = borderColor;
if (borderWidth) {
this.ctx.lineWidth = borderWidth;
}
if (UseEllipsis) {
const drawText = this.correctEllipsisText(
text,
maxWidth || this.width,
fontStyle
);
if (borderWidth) {
this.ctx.strokeText(
drawText,
this.getPixel(x),
this.getPixel(y),
maxWidth ? this.getPixel(maxWidth) : maxWidth
);
}
this.ctx.fillText(
drawText,
this.getPixel(x),
this.getPixel(y),
maxWidth ? this.getPixel(maxWidth) : maxWidth
);
} else {
const words = text.split("");
let line = "";
let yPos = y;
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i];
const textWidth = this.getTextWidth(testLine, fontStyle);
if (textWidth > this.getPixel(maxWidth || this.width)) {
if (borderWidth) {
this.ctx.strokeText(
line,
this.getPixel(x),
this.getPixel(y),
maxWidth ? this.getPixel(maxWidth) : maxWidth
);
this.ctx.fillText(
line,
this.getPixel(x),
this.getPixel(yPos),
maxWidth ? this.getPixel(maxWidth) : maxWidth
);
yPos += lineHeight * (fontSize ? this.getPixel(fontSize) : 14);
} else {
line = testLine;
}
}
}
this.ctx.strokeText(line, this.getPixel(x), this.getPixel(y));
this.ctx.fillText(line, this.getPixel(x), this.getPixel(yPos));
}
this.ctx.restore();
}
public drawLineShape({ lines, fillColor, gradients }: LineShapeDrawStep) {
if (!this.ctx || !lines.length) return;
this.ctx.save();
this.ctx.beginPath();
const [x, y] = lines[0];
this.ctx.moveTo(this.getPixel(x), this.getPixel(y));
for (let i = 1; i < lines.length; i++) {
const [ix, iy] = lines[i];
this.ctx.lineTo(this.getPixel(ix), this.getPixel(iy));
}
if (this.ctx && fillColor) {
this.ctx.fillStyle = fillColor;
this.ctx.fill();
} else if (this.ctx && gradients?.length) {
var lineargradient = this.ctx.createLinearGradient(
gradients[0],
gradients[1],
gradients[2],
gradients[3]
);
lineargradient.addColorStop(0, "#363636");
lineargradient.addColorStop(1, "white");
this.ctx.setFillStyle(lineargradient);
this.ctx.fill();
} else {
this.ctx.closePath();
}
this.ctx.restore();
}
public drawCircleShape({
x,
y,
radius,
startAngle,
endAngle,
anticlockwise,
fillColor,
}: CircleShapeDrawStep) {
if (!this.ctx) return;
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(
this.getPixel(x),
this.getPixel(y),
this.getPixel(radius),
this.getPixel(startAngle),
this.getPixel(endAngle),
anticlockwise
);
if (this.ctx && fillColor) {
this.ctx.setFillStyle(fillColor);
this.ctx.fill();
} else {
this.ctx.closePath();
}
this.ctx.restore();
}
syncDraw(): Promise<void> {
return new Promise((resolve) => {
this.ctx?.draw(true, async () => {
await sleep(30);
resolve();
});
});
}
public roundRect = ({
x,
y,
width,
height,
radius,
fillColor,
}: roundRectShapeDrawStep) => {
if (!this.ctx) return;
const dx = this.getPixel(x);
const dy = this.getPixel(y);
const dRadius = this.getPixel(radius);
const dWidth = this.getPixel(width);
const dHeight = this.getPixel(height);
this.ctx.beginPath();
this.ctx.moveTo(dx + dRadius, dy);
this.ctx.lineTo(dx + dWidth - dRadius, dy);
this.ctx.arcTo(dx + dWidth, dy, dx + dWidth, dy + dRadius, dRadius);
this.ctx.lineTo(dx + dWidth, dy + dHeight - dRadius);
this.ctx.arcTo(
dx + dWidth,
dy + dHeight,
dx + dWidth - dRadius,
dy + dHeight,
dRadius
);
this.ctx.lineTo(dx + dRadius, dy + dHeight);
this.ctx.arcTo(dx, dy + dHeight, dx, dy + dHeight - dRadius, dRadius);
this.ctx.lineTo(dx, dy + dRadius);
this.ctx.arcTo(dx, dy, dx + dRadius, dy, dRadius);
this.ctx.closePath();
this.ctx.fillStyle = fillColor;
this.ctx.fill();
};
async draw(callback?: Function) {
let index = 0;
while (index < this.drawSteps.length) {
const { type, ...otherProps } = <DrawStep>this.drawSteps[index];
const stepProps = <AnyObject>otherProps;
const props = <AnyObject>{};
Object.assign(
props,
stepProps.getProps
? await stepProps.getProps(this.drawSteps, index)
: stepProps
);
if (type === DrawType.Text) {
await this.drawText(<TextDrawStep>props);
} else if (type === DrawType.Image) {
await this.drawImage(<ImageDrawStep>props);
} else if (type === DrawType.LineShape) {
await this.drawLineShape(<LineShapeDrawStep>props);
} else if (type === DrawType.CircleShape) {
await this.drawCircleShape(<CircleShapeDrawStep>props);
} else if (type === DrawType.RoundRectShape) {
await this.roundRect(<roundRectShapeDrawStep>props);
}
props.immediateDraw && (await this.syncDraw());
index += 1;
}
this.ctx?.draw(true, (res) => {
callback?.(res);
});
}
canvas2Image(): Promise<UniApp.CanvasToTempFilePathRes> | undefined {
if (!this.ctx) return;
return new Promise((resolve) => {
uni.canvasToTempFilePath(
{
canvasId: this.canvasId,
x: 0,
y: 0,
width: this.width,
height: this.height,
success: resolve,
},
this.instanceComponent
);
});
}
clearAll() {
this.ctx?.clearRect(
0,
0,
this.getPixel(this.width),
this.getPixel(this.height)
);
this.ctx?.draw();
}
}
export default Poster;
export enum DrawType {
Text = "text",
Image = "image",
LineShape = "lineShape",
CircleShape = "circleShape",
RoundRectShape = "roundRectShape",
}
export interface InitConfig {
isPixel: boolean;
}
export type BaseDrawStep = {
x: number;
y: number;
};
export type GetProps<O> = {
getProps: (steps: DrawSteps, index: number) => O | Promise<O>;
};
export type TextDrawStep = BaseDrawStep & {
text: string;
maxWidth?: number;
color?: string;
fontSize?: number;
fontFamily?: string;
fontWeight?: number;
borderWidth?: number;
borderColor?: string;
lineHeight?: number;
UseEllipsis?: boolean;
};
export type ImageDrawStep = BaseDrawStep & {
image: string;
width: number;
height: number;
isCircle?: boolean;
clipConfig?: {
x: number;
y: number;
width: number;
height: number;
};
};
export type LineShapeDrawStep = {
lines: Array<[number, number]>;
fillColor?: string;
strokeStyle?: string;
gradients?: Array<number>;
};
export type CircleShapeDrawStep = {
x: number;
y: number;
radius: number;
startAngle: number;
endAngle: number;
anticlockwise: boolean;
fillColor?: string;
};
export type roundRectShapeDrawStep = {
ctx: CanvasRenderingContext2D;
x: number;
y: number;
width: number;
height: number;
radius: number;
fillColor: any;
};
export type DrawStep =
| ({ type: DrawType.Text } & (TextDrawStep | GetProps<TextDrawStep>))
| ({ type: DrawType.Image } & (ImageDrawStep | GetProps<ImageDrawStep>))
| ({ type: DrawType.LineShape } & (
| LineShapeDrawStep
| GetProps<LineShapeDrawStep>
));
export type DrawSteps = Array<
DrawStep & { drawData?: AnyObject; immediateDraw?: boolean }
>;
export const px2rpx = (px: number) => px / (uni.upx2px(100) / 100);
export const rpx2px = (rpx: number) => uni.upx2px(rpx);
import { getCurrentInstance, ref } from "vue";
const poster = ref<Poster | undefined | any>(undefined);
poster.value = new Poster("myCanvas", self, { isPixel: false });
const self = getCurrentInstance();
poster.value.setCanvasSize(750, wrapperHeight.value);
poster.value.clearAll();
poster.value.drawSteps = [
{
type: DrawType.Image,
getProps: (steps: DrawSteps) => {
return {
image: flurBg.path,
x: 0,
y: 0,
width: 750,
height: 308,
};
},
},
];
await poster.value.draw((res: any) => {
uni.hideLoading();
});