Commit ac4c7484 authored by rockyl's avatar rockyl

首次发布

parent 71a41d1f
# Created by .ignore support plugin (hsz.mobi)
dist
# Created by .ignore support plugin (hsz.mobi)
src
test.html
test.js
*.iml
/**
* 节点类型
*/
var NodeType;
(function (NodeType) {
NodeType[NodeType["TEXT"] = 0] = "TEXT";
NodeType[NodeType["IMAGE"] = 1] = "IMAGE";
})(NodeType || (NodeType = {}));
/**
*
* @param data
* @param callback
*/
export async function toCanvas(data, callback) {
const { nodes, width, height } = data;
var canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
//先加载完所有图片
var p = [];
nodes.forEach((n) => {
if (n.type !== NodeType.IMAGE)
return;
p.push(new Promise((r) => {
let img = new Image();
img.onload = () => {
n.img = img;
r();
};
img.src = n.src;
}));
});
if (p.length)
await Promise.all(p);
nodes.forEach((n) => {
switch (n.type) {
case NodeType.IMAGE:
drawImage(n, ctx);
break;
case NodeType.TEXT:
drawText(n, ctx);
break;
}
});
callback && callback(canvas);
return canvas;
}
/**
* 绘制图片
* @param data
* @param ctx
*/
function drawImage(data, ctx) {
const { x, y, img, width, height, borderRadius } = data;
if (borderRadius) { //有圆角,画遮罩,暂时只管一个
ctx.save();
let max = (width < height ? width : height) / 2;
let radius = Math.min(+borderRadius.replace("px", ""), max);
// ctx.beginPath();
// ctx.moveTo(x, y + radius);
// ctx.quadraticCurveTo(x, y, x + radius, y);
// ctx.lineTo(x + width - radius, y);
// ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
// ctx.lineTo(x + width, y + height - radius);
// ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
// ctx.lineTo(x + radius, y + height);
// ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
// ctx.lineTo(x, y + radius);
// ctx.clip();
ctx.beginPath();
ctx.moveTo(x + width - radius, y);
ctx.arcTo(x + width, y, x + width, y + radius, radius);
ctx.lineTo(x + width, y + height - radius);
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
ctx.lineTo(x + radius, y + height);
ctx.arcTo(x, y + height, x, y + height - radius, radius);
ctx.lineTo(x, y + radius);
ctx.arcTo(x, y, x + radius, y, radius);
ctx.closePath();
ctx.clip();
}
ctx.drawImage(img, x, y, width, height);
if (borderRadius)
ctx.restore();
}
/**
* 绘制文本,暂时没那么复杂,不用离屏玩
* @param data
* @param ctx
*/
function drawText(data, ctx) {
const { x, y, width, height, text, color, fontSize, fontWeight, wordWrap, textAlign } = data;
var font = fontSize;
font += " Arial"; //字体没有
//font-weight:bold;font-style:italic;
if (fontWeight)
font = fontWeight + " " + font;
ctx.font = font;
ctx.textBaseline = "top";
ctx.fillStyle = color;
var widthAll = ctx.measureText(text).width;
// console.log(ctx.measureText(text))
//超过宽度需要换行,且需要用到居中方式
if (wordWrap == "break-word" && widthAll > width) {
let realLines = [];
let w = ctx.measureText(text[0]).width;
let lineStr = text[0];
let wordW = 0;
let strLen = text.length;
for (let j = 1; j < strLen; j++) {
wordW = ctx.measureText(text[j]).width;
w += wordW;
if (w > width) {
realLines[realLines.length] = lineStr;
lineStr = text[j];
w = wordW;
}
else {
lineStr += text[j];
}
}
//最后一行
realLines[realLines.length] = lineStr;
ctx.textAlign = textAlign || "left";
let tx = 0;
if (ctx.textAlign == "center") {
tx = width * 0.5;
}
else if (ctx.textAlign == "right") {
tx = width;
}
//有待考虑.现在直接拿高度取平均算每行高度
var lineH = height / realLines.length;
realLines.forEach((r, i) => {
ctx.fillText(r, x + tx, y + i * lineH);
});
}
else {
ctx.textAlign = "left";
ctx.fillText(text, x, y);
}
}
/**
* Created by rockyl on 2021/1/11.
*/
const styleKeys = [
'fontSize',
'fontFamily',
'fontWeight',
'textAlign',
'color',
'wordWrap',
'borderRadius',
];
export function parseDom(el) {
const { left: pX, top: pY, width, height } = el.getBoundingClientRect();
let nodes = [];
walkNode(el, function (childNode) {
let vNode, bound, node;
if (childNode.nodeName === 'IMG') { //其他全部认为是文本
node = childNode;
vNode = {
type: 1,
src: childNode.src,
};
bound = childNode.getBoundingClientRect();
}
else {
if (childNode.data) {
node = childNode.parentElement;
let text = childNode.data.trim();
if (text) { //过滤空文本
let range = document.createRange();
range.selectNode(childNode);
bound = range.getBoundingClientRect();
range.detach();
vNode = {
type: 0,
text,
};
}
}
}
if (vNode) {
const { left, top, width, height } = bound;
vNode.x = left - pX;
vNode.y = top - pY;
vNode.width = width;
vNode.height = height;
let styles = getStylesWithoutDefaults(node);
for (let skey in styles) {
if (styleKeys.indexOf(skey) < 0) {
continue;
}
vNode[skey] = styles[skey];
}
nodes.push(vNode);
}
});
return {
width,
height,
nodes,
};
}
function walkNode(root, callback) {
callback(root);
for (let i = 0, li = root.childNodes.length; i < li; i++) {
const childNode = root.childNodes[i];
walkNode(childNode, callback);
}
}
function getStylesWithoutDefaults(element) {
let dummy = document.createElement('element-' + (new Date().getTime()));
document.body.appendChild(dummy);
let defaultStyles = getComputedStyle(dummy);
let elementStyles = getComputedStyle(element);
let diff = {};
for (let key in elementStyles) {
if (elementStyles.hasOwnProperty(key)
&& defaultStyles[key] !== elementStyles[key]) {
diff[key] = elementStyles[key];
}
}
dummy.remove();
return diff;
}
/**
* Created by rockyl on 2021/1/11.
*/
import { parseDom } from "./dom-parser.js";
import { toCanvas } from "./toCanvas.js";
let data = parseDom(document.getElementById('poster'));
console.log(data);
toCanvas(data, (c) => {
document.body.appendChild(c);
});
/**
*
* @param el html文本
* @param callback
*/
export function htmlToCanvas(el, callback) {
return toCanvas(parseDom(el), callback);
}
{ {
"name": "html-shot", "name": "html-shot",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "dist/index.js",
"license": "MIT", "types": "types/index.d.ts",
"scripts": { "license": "MIT",
"dev": "tsc -w", "scripts": {
"build": "tsc" "dev": "tsc -w",
} "build": "tsc"
}
} }
# HTML截图
实现了HtmlElement的截图功能
## Install
`yarn add html-shot`
or
`npm i html-shot`
## Usage
```js
const result = await htmlShot(document.getElementById('poster'));
let img = new Image();
img.src = result;
document.body.appendChild(img);
```
## RenderOptions
| 名称 | 类型 | 必须 | 默认值 | 说明 |
| :---- | :---- | :---- | :---- | :---- |
| `type` | 'canvas' &#124; 'jpeg' &#124; 'png' | 否 | 'png' | 导出类型 |
| `qaulity` | number | 否 | 0.7 | 导出图片质量 |
## 注意点
* 仅支持img和text两种元素
* 不支持transform变换
* 文本仅只是Arial字体,建议给元素设置为Arial字体
...@@ -12,7 +12,7 @@ const styleKeys = [ ...@@ -12,7 +12,7 @@ const styleKeys = [
'borderRadius', 'borderRadius',
] ]
export function parseDom(el: HTMLElement) { export function parseDom(el: HTMLElement = document.body) {
const {left: pX, top: pY, width, height} = el.getBoundingClientRect(); const {left: pX, top: pY, width, height} = el.getBoundingClientRect();
let nodes = []; let nodes = [];
......
...@@ -2,21 +2,16 @@ ...@@ -2,21 +2,16 @@
* Created by rockyl on 2021/1/11. * Created by rockyl on 2021/1/11.
*/ */
import { parseDom } from "./dom-parser.js"; import {parseDom} from "./dom-parser.js";
import { toCanvas } from "./toCanvas.js"; import {RenderOptions, toCanvas} from "./toCanvas.js";
let data = parseDom(document.getElementById('poster'));
console.log(data);
toCanvas(data,(c)=>{
document.body.appendChild(c)
})
/** /**
* * HTML截图
* @param el html文本 * @param {HTMLElement} [el] html节点
* @param callback * @param options 配置
* @param {Function} [callback] 回调方法
* @return Promise<string|HTMLCanvasElement> 如果是字符串则为图片的base64
*/ */
export function htmlToCanvas(el: HTMLElement,callback?:(HTMLCanvasElement)=>void):Promise<HTMLCanvasElement> { export function htmlShot(el?: HTMLElement, options?: RenderOptions, callback?: (HTMLCanvasElement) => void): Promise<HTMLCanvasElement | string> {
return toCanvas(parseDom(el),callback) return toCanvas(parseDom(el), options, callback)
} }
\ No newline at end of file
...@@ -2,171 +2,196 @@ ...@@ -2,171 +2,196 @@
* 转换接口数据 * 转换接口数据
*/ */
interface ICData { interface ICData {
width: number, width: number,
height: number, height: number,
nodes: INodeData[] nodes: INodeData[]
} }
/** /**
* 节点类型 * 节点类型
*/ */
enum NodeType { enum NodeType {
TEXT = 0, TEXT = 0,
IMAGE IMAGE
} }
/** /**
* 节点数据 * 节点数据
*/ */
interface INodeData { interface INodeData {
type: NodeType, type: NodeType,
x: number, x: number,
y: number, y: number,
width: number, width: number,
height: number, height: number,
//图片需要 //图片需要
src?: string, src?: string,
borderRadius?: string, borderRadius?: string,
img?: HTMLImageElement,//加载后挂上的 img?: HTMLImageElement,//加载后挂上的
//文本需要 //文本需要
text?: string text?: string
color?: string, color?: string,
fontSize?: string,//文本字号 fontSize?: string,//文本字号
fontWeight?: string,//文本粗细 fontWeight?: string,//文本粗细
wordWrap?: "break-word" | null wordWrap?: "break-word" | null
textAlign?: "center" | "left" | "right" textAlign?: "center" | "left" | "right"
} }
export interface RenderOptions {
type?: 'canvas' | 'jpeg' | 'png',
quality?: number,
}
/** /**
* * 渲染
* @param data * @param data
* @param callback * @param options
* @param callback
*/ */
export async function toCanvas(data: ICData, callback?: (canvas: HTMLCanvasElement) => void): Promise<HTMLCanvasElement> { export async function toCanvas(data: ICData, options: RenderOptions = {}, callback?: (canvas: HTMLCanvasElement) => void): Promise<HTMLCanvasElement | string> {
const { nodes, width, height } = data; const {type: exportType = 'png', quality = 0.7} = options;
var canvas = document.createElement("canvas");
canvas.width = width; const {nodes, width, height} = data;
canvas.height = height; let canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d"); canvas.width = width;
//先加载完所有图片 canvas.height = height;
var p: Promise<void>[] = []; let ctx = canvas.getContext("2d");
nodes.forEach((n) => { //先加载完所有图片
if (n.type !== NodeType.IMAGE) return; let p: Promise<void>[] = [];
p.push( nodes.forEach((n) => {
new Promise((r) => { if (n.type !== NodeType.IMAGE) return;
let img = new Image(); p.push(
img.onload = () => { new Promise((r) => {
n.img = img; let img = new Image();
r() img.crossOrigin = 'anonymous'
} img.onload = () => {
img.src = n.src; n.img = img;
}) r()
) }
}) img.src = n.src;
if (p.length) await Promise.all(p); })
nodes.forEach((n) => { )
switch (n.type) { })
case NodeType.IMAGE: if (p.length) await Promise.all(p);
drawImage(n, ctx) nodes.forEach((n) => {
break switch (n.type) {
case NodeType.TEXT: case NodeType.IMAGE:
drawText(n, ctx) drawImage(n, ctx)
break break
} case NodeType.TEXT:
}) drawText(n, ctx)
callback && callback(canvas) break
return canvas }
})
let result: any = canvas;
switch (exportType) {
case 'jpeg':
case 'png':
result = canvas.toDataURL('image/' + exportType, quality);
break;
}
callback && callback(result)
return result
} }
/** /**
* 绘制图片 * 绘制图片
* @param data * @param data
* @param ctx * @param ctx
*/ */
function drawImage(data: INodeData, ctx: CanvasRenderingContext2D) { function drawImage(data: INodeData, ctx: CanvasRenderingContext2D) {
const { x, y, img, width, height, borderRadius } = data; const {x, y, img, width, height, borderRadius} = data;
if (borderRadius) {//有圆角,画遮罩,暂时只管一个 if (borderRadius) {//有圆角,画遮罩,暂时只管一个
ctx.save(); ctx.save();
let max = (width < height ? width : height) / 2; let max = (width < height ? width : height) / 2;
let radius = Math.min(+borderRadius.replace("px", ""), max); let radius = Math.min(+borderRadius.replace("px", ""), max);
// ctx.beginPath(); // ctx.beginPath();
// ctx.moveTo(x, y + radius); // ctx.moveTo(x, y + radius);
// ctx.quadraticCurveTo(x, y, x + radius, y); // ctx.quadraticCurveTo(x, y, x + radius, y);
// ctx.lineTo(x + width - radius, y); // ctx.lineTo(x + width - radius, y);
// ctx.quadraticCurveTo(x + width, y, x + width, y + radius); // ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
// ctx.lineTo(x + width, y + height - radius); // ctx.lineTo(x + width, y + height - radius);
// ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); // ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
// ctx.lineTo(x + radius, y + height); // ctx.lineTo(x + radius, y + height);
// ctx.quadraticCurveTo(x, y + height, x, y + height - radius); // ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
// ctx.lineTo(x, y + radius); // ctx.lineTo(x, y + radius);
// ctx.clip(); // ctx.clip();
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x + width - radius, y); ctx.moveTo(x + width - radius, y);
ctx.arcTo(x + width, y, x + width, y + radius, radius) ctx.arcTo(x + width, y, x + width, y + radius, radius)
ctx.lineTo(x + width, y + height - radius) ctx.lineTo(x + width, y + height - radius)
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius) ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius)
ctx.lineTo(x + radius, y + height) ctx.lineTo(x + radius, y + height)
ctx.arcTo(x, y + height, x, y + height - radius, radius) ctx.arcTo(x, y + height, x, y + height - radius, radius)
ctx.lineTo(x, y + radius) ctx.lineTo(x, y + radius)
ctx.arcTo(x, y, x + radius, y, radius) ctx.arcTo(x, y, x + radius, y, radius)
ctx.closePath(); ctx.closePath();
ctx.clip(); ctx.clip();
} }
ctx.drawImage(img, x, y, width, height) ctx.drawImage(img, x, y, width, height)
if (borderRadius) ctx.restore(); if (borderRadius) ctx.restore();
} }
/** /**
* 绘制文本,暂时没那么复杂,不用离屏玩 * 绘制文本,暂时没那么复杂,不用离屏玩
* @param data * @param data
* @param ctx * @param ctx
*/ */
function drawText(data: INodeData, ctx: CanvasRenderingContext2D) { function drawText(data: INodeData, ctx: CanvasRenderingContext2D) {
const { x, y, width, height, const {
text, x, y, width, height,
color, text,
fontSize, color,
fontWeight, fontSize,
wordWrap, fontWeight,
textAlign } = data; wordWrap,
var font = fontSize; textAlign
font += " Arial";//字体没有 } = data;
//font-weight:bold;font-style:italic; let font = fontSize;
if (fontWeight) font = fontWeight + " " + font; font += " Arial";//字体没有
ctx.font = font; //font-weight:bold;font-style:italic;
ctx.textBaseline = "top"; if (fontWeight) font = fontWeight + " " + font;
ctx.fillStyle = color; ctx.font = font;
var widthAll = ctx.measureText(text).width; ctx.textBaseline = "top";
// console.log(ctx.measureText(text)) ctx.fillStyle = color;
//超过宽度需要换行,且需要用到居中方式 let widthAll = ctx.measureText(text).width;
if (wordWrap == "break-word" && widthAll > width) { // console.log(ctx.measureText(text))
let realLines = []; //超过宽度需要换行,且需要用到居中方式
let w = ctx.measureText(text[0]).width; if (wordWrap == "break-word" && widthAll > width) {
let lineStr = text[0]; let realLines = [];
let wordW = 0; let w = ctx.measureText(text[0]).width;
let strLen = text.length; let lineStr = text[0];
for (let j = 1; j < strLen; j++) { let wordW = 0;
wordW = ctx.measureText(text[j]).width; let strLen = text.length;
w += wordW; for (let j = 1; j < strLen; j++) {
if (w > width) { wordW = ctx.measureText(text[j]).width;
realLines[realLines.length] = lineStr; w += wordW;
lineStr = text[j]; if (w > width) {
w = wordW; realLines[realLines.length] = lineStr;
} else { lineStr = text[j];
lineStr += text[j]; w = wordW;
} } else {
} lineStr += text[j];
//最后一行 }
realLines[realLines.length] = lineStr; }
ctx.textAlign = textAlign || "left"; //最后一行
let tx = 0; realLines[realLines.length] = lineStr;
if (ctx.textAlign == "center") { ctx.textAlign = textAlign || "left";
tx = width * 0.5; let tx = 0;
} else if (ctx.textAlign == "right") { if (ctx.textAlign == "center") {
tx = width; tx = width * 0.5;
} } else if (ctx.textAlign == "right") {
//有待考虑.现在直接拿高度取平均算每行高度 tx = width;
var lineH = height / realLines.length; }
realLines.forEach((r, i) => { //有待考虑.现在直接拿高度取平均算每行高度
ctx.fillText(r, x + tx, y + i * lineH) let lineH = height / realLines.length;
}) realLines.forEach((r, i) => {
} else { ctx.fillText(r, x + tx, y + i * lineH)
ctx.textAlign = "left" })
ctx.fillText(text, x, y) } else {
} ctx.textAlign = "left"
} ctx.fillText(text, x, y)
\ No newline at end of file }
}
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
text-align: center; text-align: center;
width: 1.00rem; width: 1.00rem;
word-wrap: break-word; word-wrap: break-word;
font-family: Arial;
} }
</style> </style>
</head> </head>
...@@ -36,6 +37,6 @@ ...@@ -36,6 +37,6 @@
</p> </p>
</div> </div>
<script src="dist/index.js" type="module"></script> <script src="test.js" type="module"></script>
</body> </body>
</html> </html>
/**
* Created by rockyl on 2021/1/12.
*/
import {htmlShot} from "./dist/index.js";
(async function () {
const result = await htmlShot(undefined, {type: 'png'});
let img = new Image();
img.src = result;
document.body.appendChild(img);
})();
...@@ -2,7 +2,13 @@ ...@@ -2,7 +2,13 @@
"compilerOptions": { "compilerOptions": {
"module": "ES6", "module": "ES6",
"target": "ES2017", "target": "ES2017",
"outDir": "dist" "outDir": "dist",
"declaration": true,
"declarationDir": "types",
"lib": [
"DOM",
"ES2015"
]
}, },
"include": [ "include": [
"src" "src"
......
/**
* Created by rockyl on 2021/1/11.
*/
export declare function parseDom(el?: HTMLElement): {
width: number;
height: number;
nodes: any[];
};
/**
* Created by rockyl on 2021/1/11.
*/
import { RenderOptions } from "./toCanvas.js";
/**
* HTML截图
* @param {HTMLElement} [el] html节点
* @param options 配置
* @param {Function} [callback] 回调方法
* @return Promise<string|HTMLCanvasElement> 如果是字符串则为图片的base64
*/
export declare function htmlShot(el?: HTMLElement, options?: RenderOptions, callback?: (HTMLCanvasElement: any) => void): Promise<HTMLCanvasElement | string>;
/**
* 转换接口数据
*/
interface ICData {
width: number;
height: number;
nodes: INodeData[];
}
/**
* 节点类型
*/
declare enum NodeType {
TEXT = 0,
IMAGE = 1
}
/**
* 节点数据
*/
interface INodeData {
type: NodeType;
x: number;
y: number;
width: number;
height: number;
src?: string;
borderRadius?: string;
img?: HTMLImageElement;
text?: string;
color?: string;
fontSize?: string;
fontWeight?: string;
wordWrap?: "break-word" | null;
textAlign?: "center" | "left" | "right";
}
export interface RenderOptions {
type?: 'canvas' | 'jpeg' | 'png';
quality?: number;
}
/**
* 渲染
* @param data
* @param options
* @param callback
*/
export declare function toCanvas(data: ICData, options?: RenderOptions, callback?: (canvas: HTMLCanvasElement) => void): Promise<HTMLCanvasElement | string>;
export {};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment