<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>html2canvas 简易版</title>
</head>
<style>
* {
margin: 0;
padding: 0;
}
body {
display: flex;
flex-wrap: wrap;
padding: 50px;
}
#app {
position: relative;
padding-top: 10px;
padding-left: 20px;
width: 360px;
height: 640px;
border: 1px solid #000;
}
.name {
font-size: 50px;
font-weight: bold;
color: purple;
}
.avatar {
margin-right: 10px;
background: lightcyan;
border: 5px solid #04c5d3;
}
.row {
display: flex;
align-items: center;
margin-top: 30px;
}
input {
padding: 5px;
border: 2px solid #1df01d;
}
.box {
margin-top: 20px;
margin-left: 50px;
width: 100px;
height: 100px;
font-size: 20px;
color: #fff;
background: #057905;
transform: rotate(-25deg);
}
.box2 {
position: absolute;
bottom: 0;
right: 0;
width: 200px;
height: 200px;
font-size: 26px;
background: #ff00d9;
opacity: 0.3;
}
.zIndex1 {
position: absolute;
padding: 0 15px;
top: 50px;
right: 0;
font-size: 30px;
border: 1px solid red;
z-index: 1;
}
.zIndex-1 {
position: absolute;
padding: 0 15px;
bottom: 230px;
right: 0;
font-size: 30px;
border: 1px solid red;
z-index: -1;
}
</style>
<body>
<div id="app">
<div class="name">都反到反的</div>
<img class="avatar" src="https://p26-passport.byteacctimg.com/img/user-avatar/35c5f1e45b9837645389693e68b84ef7~40x40.awebp" />
<div class="row">
<div class="box">来了</div>
<div>
<div class="box2"></div>
</div>
</div>
<div class="row">
<span>input:</span>
<input value="输入内容" />
</div>
<div class="zIndex1">zIndex: 1</div>
<div class="zIndex-1">zIndex: -1</div>
</div>
<div>
<button onclick="handleClick()">点击生成html2Canvas</button>
</div>
<script>
class html2canvas {
constructor(el) {
this.el = el;
this.global = this.formatGlobal(this.el);
this.root = this.parseTree(this.global, this.el);
const stack = this.parseStackingContext(this.root);
this.createCanvas(this.el);
this.render(stack);
document.body.appendChild(this.canvas);
console.log('====== 解析 html 之后的结果 ======');
console.log(this.root);
console.log('====== 层叠上下文分组后的结果 ======');
console.log(stack);
}
formatGlobal(el) {
const { left, top } = el.getBoundingClientRect();
return {
offset: { x: left, y: top },
};
}
parseTree(global, el) {
const container = this.createContainer(global, el);
this.parseNodeTree(global, el, container);
return container;
}
parseNodeTree(global, el, parent) {
[...el.childNodes].map((child) => {
if (child.nodeType === 3) {
if (child.textContent.trim().length > 0) {
const textElContainer = new TextElContainer(child.textContent, parent);
parent.textNodes.push(textElContainer);
}
} else {
const container = this.createContainer(global, child);
const { position, zIndex, opacity, transform } = container.styles;
if ((position !== 'static' && !isNaN(zIndex)) || opacity < 1 || transform !== 'none') {
container.flags = 1;
}
parent.elements.push(container);
this.parseNodeTree(global, child, container);
}
});
}
createContainer(global, el) {
if (el.tagName === 'IMG') {
return new ImageElContainer(global, el);
} else if (el.tagName === 'INPUT') {
return new InputElContainer(global, el);
} else {
return new ElContainer(global, el);
}
}
parseStackingContext(container) {
const root = new StackingContext(container);
this.parseStackTree(container, root);
return root;
}
parseStackTree(parent, stackingContext) {
parent.elements.map((child) => {
if (child.flags) {
const stack = new StackingContext(child);
const zIndex = child.styles.zIndex;
if (zIndex > 0) {
stackingContext.positiveZIndex.push(stack);
} else if (zIndex < 0) {
stackingContext.negativeZIndex.push(stack);
} else {
stackingContext.zeroOrAutoZIndexOrTransformedOrOpacity.push(stack);
}
this.parseStackTree(child, stack);
} else {
if (child.styles.display.indexOf('inline') >= 0) {
stackingContext.inlineLevel.push(child);
} else {
stackingContext.nonInlineLevel.push(child);
}
this.parseStackTree(child, stackingContext);
}
});
}
render(stack) {
const { negativeZIndex = [], nonInlineLevel = [], inlineLevel = [], positiveZIndex = [], zeroOrAutoZIndexOrTransformedOrOpacity = [] } = stack;
this.ctx2d.save();
this.setTransformAndOpacity(stack.container);
this.renderNodeBackgroundAndBorders(stack.container);
negativeZIndex.map((el) => this.render(el));
this.renderNodeContent(stack.container);
nonInlineLevel.map((el) => this.renderNode(el));
inlineLevel.map((el) => this.renderNode(el));
zeroOrAutoZIndexOrTransformedOrOpacity.map((el) => this.render(el));
positiveZIndex.map((el) => this.render(el));
this.ctx2d.restore();
}
renderNodeContent(container) {
if (container.textNodes.length) {
container.textNodes.map((text) => this.renderText(text, container.styles));
} else if (container instanceof ImageElContainer) {
this.renderImg(container);
} else if (container instanceof InputElContainer) {
this.renderInput(container);
}
}
renderNode(container) {
this.renderNodeBackgroundAndBorders(container);
this.renderNodeContent(container);
}
setTransformAndOpacity(container) {
const { bounds, styles } = container;
const { ctx2d } = this;
const { transform, opacity, transformOrigin } = styles;
if (opacity < 1.0) {
ctx2d.globalAlpha = opacity;
}
if (transform !== 'none') {
const origin = transformOrigin.split(' ').map((_) => parseInt(_, 10));
const offsetX = bounds.left + origin[0];
const offsetY = bounds.top + origin[1];
const matrix = transform.slice(7, -1).split(', ').map(Number);
ctx2d.translate(offsetX, offsetY);
ctx2d.transform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
ctx2d.translate(-offsetX, -offsetY);
container.transform = {
offsetX,
offsetY,
matrix,
};
}
}
renderNodeBackgroundAndBorders(container) {
const { bounds, styles } = container;
const { ctx2d } = this;
const bg = styles.backgroundColor;
const borderWidth = parseInt(styles.borderWidth);
const { top, left, width, height } = bounds;
let points = [
[left, top],
[left + width, top],
[left + width, top + height],
[left, top + height],
];
if (container.transform) {
const { offsetX, offsetY } = container.transform;
const width = parseInt(styles.width);
const height = parseInt(styles.height);
points = [
[offsetX - width / 2, offsetY - height / 2],
[offsetX + width / 2, offsetY - height / 2],
[offsetX + width / 2, offsetY + height / 2],
[offsetX - width / 2, offsetY + height / 2],
];
}
const bgArr = bg.slice(5, -1).split(', ');
if (bgArr[bgArr.length - 1]) {
ctx2d.save();
ctx2d.beginPath();
this.drawPathByPoints(points);
ctx2d.closePath();
ctx2d.fillStyle = bg;
ctx2d.fill();
ctx2d.restore();
}
if (borderWidth) {
ctx2d.save();
ctx2d.beginPath();
this.drawPathByPoints(points);
ctx2d.closePath();
ctx2d.lineWidth = borderWidth;
ctx2d.strokeStyle = styles.borderColor;
ctx2d.stroke();
ctx2d.restore();
}
}
renderText(text, styles) {
const { ctx2d } = this;
ctx2d.save();
ctx2d.font = `${styles.fontWeight} ${styles.fontSize} ${styles.fontFamily}`;
ctx2d.fillStyle = styles.color;
ctx2d.fillText(text.text, text.bounds.left, text.bounds.top);
ctx2d.restore();
}
renderImg(container) {
const { ctx2d } = this;
const { el, bounds, styles } = container;
ctx2d.drawImage(el, 0, 0, parseInt(styles.width), parseInt(styles.height), bounds.left, bounds.top, bounds.width, bounds.height);
}
renderInput(container) {
const { value, bounds, styles } = container;
const { paddingLeft, paddingTop, fontSize } = styles;
const text = {
text: value,
bounds: {
...bounds,
top: bounds.top + parseInt(paddingTop) + parseInt(fontSize),
left: bounds.left + parseInt(paddingLeft),
},
};
this.renderText(text, styles);
}
drawPathByPoints(points) {
points.map((point, i) => {
if (i === 0) {
this.ctx2d.moveTo(point[0], point[1]);
} else {
this.ctx2d.lineTo(point[0], point[1]);
}
});
}
createCanvas(el) {
const { width, height } = el.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const canvas = document.createElement('canvas');
const ctx2d = canvas.getContext('2d');
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
ctx2d.scale(dpr, dpr);
this.canvas = canvas;
this.ctx2d = ctx2d;
return canvas;
}
}
class Bounds {
constructor(global, el) {
const { x = 0, y = 0 } = global.offset;
const { top, left, width, height } = el.getBoundingClientRect();
this.top = top - y;
this.left = left - x;
this.width = width;
this.height = height;
}
}
class ElContainer {
constructor(global, el) {
this.styles = window.getComputedStyle(el);
const transform = this.styles.transform;
if (transform !== 'none') {
el.style.transform = 'none';
}
this.bounds = new Bounds(global, el);
if (transform !== 'none') el.style.transform = transform;
this.elements = [];
this.textNodes = [];
this.flags = 0;
this.el = el;
}
}
class ImageElContainer extends ElContainer {
constructor(global, el) {
super(global, el);
this.src = el.src;
}
}
class InputElContainer extends ElContainer {
constructor(global, el) {
super(global, el);
this.type = el.type.toLowerCase();
this.value = el.value;
}
}
class TextElContainer {
constructor(text, parent) {
this.bounds = this.getTrueBounds(parent);
this.text = text;
this.parent = parent;
}
getTrueBounds(parent) {
let { top, left, width, height } = parent.bounds;
const { paddingLeft, paddingTop, borderWidth, fontSize } = parent.styles;
top = top + parseInt(paddingTop) + parseInt(borderWidth) + parseInt(fontSize);
left = left + parseInt(paddingLeft) + parseInt(borderWidth);
return {
top,
left,
width,
height,
};
}
}
class StackingContext {
constructor(container) {
this.container = container;
this.negativeZIndex = [];
this.nonInlineLevel = [];
this.nonPositionedFloats = [];
this.inlineLevel = [];
this.positiveZIndex = [];
this.zeroOrAutoZIndexOrTransformedOrOpacity = [];
}
}
</script>
<script>
function handleClick() {
new html2canvas(document.getElementById('app'));
}
</script>
</body>
</html>