Commit 2a777ad9 authored by rockyl's avatar rockyl

init

parent 7a7dcc5b
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
* Created by rockyl on 2021/1/26. * Created by rockyl on 2021/1/26.
*/ */
export const debugMode = window['html-shot/debug-mode'] || false; export const debugMode = window['html-shot/debug-mode'] || false
/** /**
* Created by rockyl on 2021/1/11. * Created by rockyl on 2021/1/11.
*/ */
import {debugMode} from "./config.js"; import {debugMode} from "./config.js"
const commonStyleKeys = [ const commonStyleKeys = [
'backgroundColor', 'backgroundColor',
...@@ -26,15 +26,15 @@ const includeStyleKeys = [ ...@@ -26,15 +26,15 @@ const includeStyleKeys = [
] ]
export function parseDom(el: HTMLElement = document.body) { 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 = []
walkNode(el, function (childNode) { walkNode(el, function (childNode) {
let vNode, bound, node, isText; let vNode, bound, node, isText
let skip = false; let skip = false
switch (childNode.nodeName) { switch (childNode.nodeName) {
case 'IMG': case 'IMG':
node = childNode; node = childNode
const src = node.getAttribute('src') //必须使用此方法,使用node.src且为空时会返回当前页面链接 const src = node.getAttribute('src') //必须使用此方法,使用node.src且为空时会返回当前页面链接
if (src) { //过滤src为空的图片 if (src) { //过滤src为空的图片
vNode = { vNode = {
...@@ -42,46 +42,46 @@ export function parseDom(el: HTMLElement = document.body) { ...@@ -42,46 +42,46 @@ export function parseDom(el: HTMLElement = document.body) {
src, src,
} }
} }
break; break
case 'CANVAS': case 'CANVAS':
node = childNode; node = childNode
vNode = { vNode = {
type: 3, type: 3,
img: node, img: node,
} }
break; break
case '#text': case '#text':
isText = true; isText = true
node = childNode.parentElement; node = childNode.parentElement
let text = childNode.data.trim(); let text = childNode.data.trim()
if (text) { //过滤空文本 if (text) { //过滤空文本
let range = document.createRange(); let range = document.createRange()
range.selectNode(childNode); range.selectNode(childNode)
bound = range.getBoundingClientRect(); bound = range.getBoundingClientRect()
range.detach(); range.detach()
vNode = { vNode = {
type: 1, type: 1,
text, text,
} }
} }
break; break
case '#comment': case '#comment':
skip = true; skip = true
break; break
default: default:
break; break
} }
if (skip) { if (skip) {
return; return
} }
if (!bound && childNode.getBoundingClientRect) { if (!bound && childNode.getBoundingClientRect) {
bound = childNode.getBoundingClientRect(); bound = childNode.getBoundingClientRect()
} }
let styles = getStylesWithoutDefaults(node || childNode, includeStyleKeys); let styles = getStylesWithoutDefaults(node || childNode, includeStyleKeys)
if (!isText) { if (!isText) {
for (let skey of commonStyleKeys) { for (let skey of commonStyleKeys) {
...@@ -95,64 +95,64 @@ export function parseDom(el: HTMLElement = document.body) { ...@@ -95,64 +95,64 @@ export function parseDom(el: HTMLElement = document.body) {
} }
} }
if (styles[skey] !== undefined) { if (styles[skey] !== undefined) {
vNode[skey] = styles[skey]; vNode[skey] = styles[skey]
} }
} }
} }
if (vNode && bound) { if (vNode && bound) {
const {left, top, width, height} = bound; const {left, top, width, height} = bound
vNode.x = left - pX; vNode.x = left - pX
vNode.y = top - pY; vNode.y = top - pY
vNode.width = width; vNode.width = width
vNode.height = height; vNode.height = height
for (let skey in styles) { for (let skey in styles) {
if (styleKeys.indexOf(skey) < 0) { if (styleKeys.indexOf(skey) < 0) {
continue continue
} }
vNode[skey] = styles[skey]; vNode[skey] = styles[skey]
} }
nodes.push(vNode); nodes.push(vNode)
} }
}); })
if (debugMode) { if (debugMode) {
console.info(nodes); console.info(nodes)
} }
return { return {
width, width,
height, height,
nodes, nodes,
}; }
} }
function walkNode(root, callback) { function walkNode(root, callback) {
callback(root); callback(root)
for (let i = 0, li = root.childNodes.length; i < li; i++) { for (let i = 0, li = root.childNodes.length; i < li; i++) {
const childNode = root.childNodes[i]; const childNode = root.childNodes[i]
walkNode(childNode, callback); walkNode(childNode, callback)
} }
} }
function getStylesWithoutDefaults(element, includes: string[] = []): any { function getStylesWithoutDefaults(element, includes: string[] = []): any {
let dummy = document.createElement('element-' + (new Date().getTime())); let dummy = document.createElement('element-' + (new Date().getTime()))
document.body.appendChild(dummy); document.body.appendChild(dummy)
let defaultStyles = getComputedStyle(dummy); let defaultStyles = getComputedStyle(dummy)
let elementStyles = getComputedStyle(element); let elementStyles = getComputedStyle(element)
let diff = {}; let diff = {}
for (let key in elementStyles) { for (let key in elementStyles) {
if (includes.indexOf(key) >= 0 || (elementStyles.hasOwnProperty(key) && defaultStyles[key] !== elementStyles[key])) { if (includes.indexOf(key) >= 0 || (elementStyles.hasOwnProperty(key) && defaultStyles[key] !== elementStyles[key])) {
diff[key] = elementStyles[key]; diff[key] = elementStyles[key]
} }
} }
dummy.remove(); dummy.remove()
return diff; return diff
} }
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
* 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 {RenderOptions, toCanvas} from "./toCanvas.js"; import {RenderOptions, toCanvas} from "./toCanvas.js"
/** /**
* HTML截图 * HTML截图
......
...@@ -61,33 +61,33 @@ export interface RenderOptions { ...@@ -61,33 +61,33 @@ export interface RenderOptions {
* @return Promise<HTMLCanvasElement | string> * @return Promise<HTMLCanvasElement | string>
*/ */
export async function toCanvas(data: ICData, options: RenderOptions = {}, callback?: (canvas: HTMLCanvasElement) => void): Promise<HTMLCanvasElement | string> { export async function toCanvas(data: ICData, options: RenderOptions = {}, callback?: (canvas: HTMLCanvasElement) => void): Promise<HTMLCanvasElement | string> {
const {type: exportType = 'png', quality = 0.7} = options; const {type: exportType = 'png', quality = 0.7} = options
let {scale = window['devicePixelRatio'] || 1} = options; let {scale = window['devicePixelRatio'] || 1} = options
let {nodes, width, height} = data; let {nodes, width, height} = data
width *= scale; width *= scale
height *= scale; height *= scale
let canvas = document.createElement("canvas"); let canvas = document.createElement("canvas")
canvas.width = width; canvas.width = width
canvas.height = height; canvas.height = height
let ctx = canvas.getContext("2d"); let ctx = canvas.getContext("2d")
//先加载完所有图片 //先加载完所有图片
let p: Promise<void>[] = []; let p: Promise<void>[] = []
nodes.forEach((n) => { nodes.forEach((n) => {
if (n.type == NodeType.IMAGE) {//图片标签 if (n.type == NodeType.IMAGE) {//图片标签
p.push( p.push(
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
let img = new Image(); let img = new Image()
img.crossOrigin = 'anonymous' img.crossOrigin = 'anonymous'
img.onload = () => { img.onload = () => {
n.img = img; n.img = img
resolve() resolve()
} }
img.onerror = () => { img.onerror = () => {
reject(`html shot error: can't fetch image[${img.src}]`); reject(`html shot error: can't fetch image[${img.src}]`)
} }
img.src = n.src; img.src = n.src
}) })
) )
} }
...@@ -95,18 +95,18 @@ export async function toCanvas(data: ICData, options: RenderOptions = {}, callba ...@@ -95,18 +95,18 @@ export async function toCanvas(data: ICData, options: RenderOptions = {}, callba
if (n.backgroundImage) { if (n.backgroundImage) {
p.push( p.push(
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
let img = new Image(); let img = new Image()
img.crossOrigin = 'anonymous' img.crossOrigin = 'anonymous'
img.onload = () => { img.onload = () => {
n.imgBg = img; n.imgBg = img
resolve() resolve()
} }
img.onerror = () => { img.onerror = () => {
reject(`html shot error: can't fetch image[${img.src}]`); reject(`html shot error: can't fetch image[${img.src}]`)
} }
let result = n.backgroundImage.match(/url\((.*)*\)/); let result = n.backgroundImage.match(/url\((.*)*\)/)
if (result) { if (result) {
img.src = result[1].replace(/['"]/g, ''); img.src = result[1].replace(/['"]/g, '')
} else { } else {
reject(`html shot error: can't parse ${n.backgroundImage}`) reject(`html shot error: can't parse ${n.backgroundImage}`)
} }
...@@ -114,13 +114,13 @@ export async function toCanvas(data: ICData, options: RenderOptions = {}, callba ...@@ -114,13 +114,13 @@ export async function toCanvas(data: ICData, options: RenderOptions = {}, callba
) )
} }
}) })
if (p.length) await Promise.all(p); if (p.length) await Promise.all(p)
nodes.forEach((n) => { nodes.forEach((n) => {
//通用属性先绘制,背景颜色,边框等等 //通用属性先绘制,背景颜色,边框等等
//背景颜色 //背景颜色
if (n.backgroundColor) drawBackgroundColor(n, ctx, scale) if (n.backgroundColor) drawBackgroundColor(n, ctx, scale)
//背景图片 //背景图片
if (n.backgroundImage) ctx.drawImage(n.imgBg, n.x * scale, n.y * scale, n.width * scale, n.height * scale); if (n.backgroundImage) ctx.drawImage(n.imgBg, n.x * scale, n.y * scale, n.width * scale, n.height * scale)
//边框 //边框
if (n.borderWidth) drawBorder(n, ctx, scale) if (n.borderWidth) drawBorder(n, ctx, scale)
...@@ -135,12 +135,12 @@ export async function toCanvas(data: ICData, options: RenderOptions = {}, callba ...@@ -135,12 +135,12 @@ export async function toCanvas(data: ICData, options: RenderOptions = {}, callba
} }
}) })
let result: any = canvas; let result: any = canvas
switch (exportType) { switch (exportType) {
case 'jpeg': case 'jpeg':
case 'png': case 'png':
result = canvas.toDataURL('image/' + exportType, quality); result = canvas.toDataURL('image/' + exportType, quality)
break; break
} }
callback && callback(result) callback && callback(result)
...@@ -154,20 +154,20 @@ export async function toCanvas(data: ICData, options: RenderOptions = {}, callba ...@@ -154,20 +154,20 @@ export async function toCanvas(data: ICData, options: RenderOptions = {}, callba
* @param scale * @param scale
*/ */
function drawImage(data: INodeData, ctx: CanvasRenderingContext2D, scale: number) { function drawImage(data: INodeData, ctx: CanvasRenderingContext2D, scale: number) {
let {x, y, img, width, height, borderRadius: borderRadiusTxt} = data; let {x, y, img, width, height, borderRadius: borderRadiusTxt} = data
x *= scale; x *= scale
y *= scale; y *= scale
width *= scale; width *= scale
height *= scale; height *= scale
let borderRadius = parseFloat(borderRadiusTxt) * scale; let borderRadius = parseFloat(borderRadiusTxt) * scale
if (borderRadius) {//有圆角,画遮罩,暂时只管一个 if (borderRadius) {//有圆角,画遮罩,暂时只管一个
ctx.save(); ctx.save()
borderRadiusPath(data, ctx, scale) borderRadiusPath(data, ctx, scale)
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()
} }
/** /**
...@@ -186,58 +186,58 @@ function drawText(data: INodeData, ctx: CanvasRenderingContext2D, scale: number) ...@@ -186,58 +186,58 @@ function drawText(data: INodeData, ctx: CanvasRenderingContext2D, scale: number)
fontStyle, fontStyle,
wordWrap, wordWrap,
textAlign textAlign
} = data; } = data
x *= scale; x *= scale
y *= scale; y *= scale
width *= scale; width *= scale
height *= scale; height *= scale
let fontSize = parseFloat(fontSizeTxt) * scale; let fontSize = parseFloat(fontSizeTxt) * scale
let font = fontSize + 'px'; let font = fontSize + 'px'
font += " Arial";//字体没有 font += " Arial";//字体没有
//font-weight:bold;font-style:italic; //font-weight:bold;font-style:italic
if (fontWeight) font = fontWeight + " " + font; if (fontWeight) font = fontWeight + " " + font
if (fontStyle) font = fontStyle + " " + font; if (fontStyle) font = fontStyle + " " + font
ctx.font = font; ctx.font = font
ctx.textBaseline = "bottom"; ctx.textBaseline = "bottom"
ctx.fillStyle = color||"black"; ctx.fillStyle = color||"black"
var textMetrics = ctx.measureText(text); var textMetrics = ctx.measureText(text)
let widthAll = textMetrics.width; let widthAll = textMetrics.width
// var actualBoundingBoxAscent = textMetrics.actualBoundingBoxAscent; // var actualBoundingBoxAscent = textMetrics.actualBoundingBoxAscent
// console.log(actualBoundingBoxAscent) // console.log(actualBoundingBoxAscent)
// console.log(ctx.measureText(text)) // console.log(ctx.measureText(text))
//超过宽度需要换行,且需要用到居中方式 //超过宽度需要换行,且需要用到居中方式
if (wordWrap == "break-word" && widthAll > width) { if (wordWrap == "break-word" && widthAll > width) {
let realLines = []; let realLines = []
let w = ctx.measureText(text[0]).width; let w = ctx.measureText(text[0]).width
let lineStr = text[0]; let lineStr = text[0]
let wordW = 0; let wordW = 0
let strLen = text.length; let strLen = text.length
for (let j = 1; j < strLen; j++) { for (let j = 1; j < strLen; j++) {
wordW = ctx.measureText(text[j]).width; wordW = ctx.measureText(text[j]).width
w += wordW; w += wordW
if (w > width) { if (w > width) {
realLines[realLines.length] = lineStr; realLines[realLines.length] = lineStr
lineStr = text[j]; lineStr = text[j]
w = wordW; w = wordW
} else { } else {
lineStr += text[j]; lineStr += text[j]
} }
} }
//最后一行 //最后一行
realLines[realLines.length] = lineStr; realLines[realLines.length] = lineStr
ctx.textAlign = textAlign || "left"; ctx.textAlign = textAlign || "left"
let tx = 0; let tx = 0
if (ctx.textAlign == "center") { if (ctx.textAlign == "center") {
tx = width * 0.5; tx = width * 0.5
} else if (ctx.textAlign == "right") { } else if (ctx.textAlign == "right") {
tx = width; tx = width
} }
//有待考虑.现在直接拿高度取平均算每行高度 //有待考虑.现在直接拿高度取平均算每行高度
let lineH = height / realLines.length; let lineH = height / realLines.length
realLines.forEach((r, i) => { realLines.forEach((r, i) => {
ctx.fillText(r, x + tx, y + i * lineH + lineH) ctx.fillText(r, x + tx, y + i * lineH + lineH)
}) })
...@@ -253,17 +253,17 @@ function drawBackgroundColor(data: INodeData, ctx: CanvasRenderingContext2D, sca ...@@ -253,17 +253,17 @@ function drawBackgroundColor(data: INodeData, ctx: CanvasRenderingContext2D, sca
x, y, width, height, x, y, width, height,
backgroundColor, backgroundColor,
borderRadius: borderRadiusTxt borderRadius: borderRadiusTxt
} = data; } = data
x *= scale; x *= scale
y *= scale; y *= scale
width *= scale; width *= scale
height *= scale; height *= scale
ctx.fillStyle = backgroundColor; ctx.fillStyle = backgroundColor
let borderRadius = parseFloat(borderRadiusTxt) * scale; let borderRadius = parseFloat(borderRadiusTxt) * scale
if (borderRadius) {//有圆角,画遮罩,暂时只管一个 if (borderRadius) {//有圆角,画遮罩,暂时只管一个
borderRadiusPath(data, ctx, scale) borderRadiusPath(data, ctx, scale)
ctx.fill(); ctx.fill()
} else { } else {
ctx.fillRect(x, y, width, height) ctx.fillRect(x, y, width, height)
} }
...@@ -276,21 +276,21 @@ function drawBorder(data: INodeData, ctx: CanvasRenderingContext2D, scale: numbe ...@@ -276,21 +276,21 @@ function drawBorder(data: INodeData, ctx: CanvasRenderingContext2D, scale: numbe
borderRadius: borderRadiusTxt, borderRadius: borderRadiusTxt,
borderWidth: borderWidthTxt, borderWidth: borderWidthTxt,
borderColor borderColor
} = data; } = data
let borderRadius = parseFloat(borderRadiusTxt) * scale; let borderRadius = parseFloat(borderRadiusTxt) * scale
let borderWidth = parseFloat(borderWidthTxt) * scale; let borderWidth = parseFloat(borderWidthTxt) * scale
x *= scale; x *= scale
y *= scale; y *= scale
width *= scale; width *= scale
height *= scale; height *= scale
ctx.lineWidth = borderWidth; ctx.lineWidth = borderWidth
ctx.lineCap = "round"; ctx.lineCap = "round"
ctx.lineJoin = "round"; ctx.lineJoin = "round"
ctx.miterLimit = 0; ctx.miterLimit = 0
ctx.strokeStyle = borderColor; ctx.strokeStyle = borderColor
if (borderRadius) { if (borderRadius) {
borderRadiusPath(data, ctx, scale) borderRadiusPath(data, ctx, scale)
ctx.stroke(); ctx.stroke()
} else { } else {
ctx.strokeRect(x, y, width, height) ctx.strokeRect(x, y, width, height)
} }
...@@ -300,30 +300,30 @@ function drawBorder(data: INodeData, ctx: CanvasRenderingContext2D, scale: numbe ...@@ -300,30 +300,30 @@ function drawBorder(data: INodeData, ctx: CanvasRenderingContext2D, scale: numbe
function borderRadiusPath(data: INodeData, ctx: CanvasRenderingContext2D, scale: number) { function borderRadiusPath(data: INodeData, ctx: CanvasRenderingContext2D, scale: number) {
let { let {
x, y, width, height, borderRadius: borderRadiusTxt x, y, width, height, borderRadius: borderRadiusTxt
} = data; } = data
x *= scale; x *= scale
y *= scale; y *= scale
width *= scale; width *= scale
height *= scale; height *= scale
let borderRadius = parseFloat(borderRadiusTxt) * scale; let borderRadius = parseFloat(borderRadiusTxt) * scale
if (borderRadius) {//有圆角,画遮罩,暂时只管一个 if (borderRadius) {//有圆角,画遮罩,暂时只管一个
let max = (width < height ? width : height) / 2; let max = (width < height ? width : height) / 2
let radius = Math.min(borderRadius, max); let radius = Math.min(borderRadius, 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)
...@@ -331,6 +331,6 @@ function borderRadiusPath(data: INodeData, ctx: CanvasRenderingContext2D, scale: ...@@ -331,6 +331,6 @@ function borderRadiusPath(data: INodeData, ctx: CanvasRenderingContext2D, scale:
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()
} }
} }
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