html2canvas-pro
Version:
Screenshots with JavaScript. Next generation!
315 lines • 16.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseStackingContexts = exports.ElementPaint = exports.StackingContext = void 0;
const bitwise_1 = require("../core/bitwise");
const bound_curves_1 = require("./bound-curves");
const effects_1 = require("./effects");
const path_1 = require("./path");
const ol_element_container_1 = require("../dom/elements/ol-element-container");
const li_element_container_1 = require("../dom/elements/li-element-container");
const counter_1 = require("../css/types/functions/counter");
const length_percentage_1 = require("../css/types/length-percentage");
class StackingContext {
constructor(container) {
this.element = container;
this.inlineLevel = [];
this.nonInlineLevel = [];
this.negativeZIndex = [];
this.zeroOrAutoZIndexOrTransformedOrOpacity = [];
this.positiveZIndex = [];
this.nonPositionedFloats = [];
this.nonPositionedInlineLevel = [];
}
}
exports.StackingContext = StackingContext;
class ElementPaint {
constructor(container, parent) {
this.container = container;
this.parent = parent;
this.effects = [];
this.curves = new bound_curves_1.BoundCurves(this.container);
if (this.container.styles.opacity < 1) {
this.effects.push(new effects_1.OpacityEffect(this.container.styles.opacity));
}
if (this.container.styles.rotate !== null) {
const origin = this.container.styles.transformOrigin;
const offsetX = this.container.bounds.left + (0, length_percentage_1.getAbsoluteValue)(origin[0], this.container.bounds.width);
const offsetY = this.container.bounds.top + (0, length_percentage_1.getAbsoluteValue)(origin[1], this.container.bounds.height);
// Apply rotate property if present
const angle = this.container.styles.rotate;
const rad = (angle * Math.PI) / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
const rotateMatrix = [cos, sin, -sin, cos, 0, 0];
this.effects.push(new effects_1.TransformEffect(offsetX, offsetY, rotateMatrix));
}
if (this.container.styles.transform !== null) {
const origin = this.container.styles.transformOrigin;
const offsetX = this.container.bounds.left + (0, length_percentage_1.getAbsoluteValue)(origin[0], this.container.bounds.width);
const offsetY = this.container.bounds.top + (0, length_percentage_1.getAbsoluteValue)(origin[1], this.container.bounds.height);
const matrix = this.container.styles.transform;
this.effects.push(new effects_1.TransformEffect(offsetX, offsetY, matrix));
}
if (this.container.styles.overflowX !== 0 /* OVERFLOW.VISIBLE */) {
const borderBox = (0, bound_curves_1.calculateBorderBoxPath)(this.curves);
const paddingBox = (0, bound_curves_1.calculatePaddingBoxPath)(this.curves);
if ((0, path_1.equalPath)(borderBox, paddingBox)) {
this.effects.push(new effects_1.ClipEffect(borderBox, 2 /* EffectTarget.BACKGROUND_BORDERS */ | 4 /* EffectTarget.CONTENT */));
}
else {
this.effects.push(new effects_1.ClipEffect(borderBox, 2 /* EffectTarget.BACKGROUND_BORDERS */));
this.effects.push(new effects_1.ClipEffect(paddingBox, 4 /* EffectTarget.CONTENT */));
}
}
if (this.container.styles.clipPath.type !== 0 /* CLIP_PATH_TYPE.NONE */) {
const clipPathEffect = buildClipPathEffect(this.container.styles.clipPath, this.container.bounds);
if (clipPathEffect) {
this.effects.push(clipPathEffect);
}
}
}
getEffects(target) {
let inFlow = [2 /* POSITION.ABSOLUTE */, 3 /* POSITION.FIXED */].indexOf(this.container.styles.position) === -1;
let parent = this.parent;
const effects = this.effects.slice(0);
while (parent) {
const croplessEffects = parent.effects.filter((effect) => !(0, effects_1.isClipEffect)(effect));
if (inFlow || parent.container.styles.position !== 0 /* POSITION.STATIC */ || !parent.parent) {
inFlow = [2 /* POSITION.ABSOLUTE */, 3 /* POSITION.FIXED */].indexOf(parent.container.styles.position) === -1;
if (parent.container.styles.overflowX !== 0 /* OVERFLOW.VISIBLE */) {
const borderBox = (0, bound_curves_1.calculateBorderBoxPath)(parent.curves);
const paddingBox = (0, bound_curves_1.calculatePaddingBoxPath)(parent.curves);
if (!(0, path_1.equalPath)(borderBox, paddingBox)) {
effects.unshift(new effects_1.ClipEffect(paddingBox, 2 /* EffectTarget.BACKGROUND_BORDERS */ | 4 /* EffectTarget.CONTENT */));
}
}
effects.unshift(...croplessEffects);
}
else {
effects.unshift(...croplessEffects);
}
parent = parent.parent;
}
return effects.filter((effect) => (0, bitwise_1.contains)(effect.target, target));
}
}
exports.ElementPaint = ElementPaint;
/**
* Resolve a `closest-side` or `farthest-side` shape-radius keyword to pixels
* for a single axis. Used by both `circle()` (per-axis) and `ellipse()`.
*
* @param r - The ShapeRadius (keyword or length-percentage).
* @param center - Absolute center coordinate on this axis (cx or cy).
* @param start - Absolute start of the reference box on this axis.
* @param end - Absolute end of the reference box on this axis.
* @param dimRef - Reference dimension for resolving a length-percentage value.
*/
const resolveAxisRadius = (r, center, start, end, dimRef) => {
if (r === 'closest-side')
return Math.min(center - start, end - center);
if (r === 'farthest-side')
return Math.max(center - start, end - center);
return (0, length_percentage_1.getAbsoluteValue)(r, dimRef);
};
/**
* Convert a parsed ClipPathValue + element bounds into a ClipPathEffect whose
* `applyClip` callback draws the clip shape directly onto the canvas context.
*
* All coordinates are computed in page-absolute space at construction time so
* the callback itself is allocation-free and executes synchronously.
*/
const buildClipPathEffect = (clipPath, bounds) => {
const { left: bLeft, top: bTop, width: bWidth, height: bHeight } = bounds;
switch (clipPath.type) {
case 1 /* CLIP_PATH_TYPE.INSET */: {
const iLeft = (0, length_percentage_1.getAbsoluteValue)(clipPath.left, bWidth);
const iTop = (0, length_percentage_1.getAbsoluteValue)(clipPath.top, bHeight);
const x = bLeft + iLeft;
const y = bTop + iTop;
// Clamp to zero: per CSS spec, overlapping insets produce an empty shape.
const w = Math.max(0, bWidth - iLeft - (0, length_percentage_1.getAbsoluteValue)(clipPath.right, bWidth));
const h = Math.max(0, bHeight - iTop - (0, length_percentage_1.getAbsoluteValue)(clipPath.bottom, bHeight));
return new effects_1.ClipPathEffect((ctx) => {
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.clip();
});
}
case 2 /* CLIP_PATH_TYPE.CIRCLE */: {
const cx = bLeft + (0, length_percentage_1.getAbsoluteValue)(clipPath.cx, bWidth);
const cy = bTop + (0, length_percentage_1.getAbsoluteValue)(clipPath.cy, bHeight);
let r;
if (clipPath.radius === 'closest-side') {
r = Math.min(cx - bLeft, cy - bTop, bLeft + bWidth - cx, bTop + bHeight - cy);
}
else if (clipPath.radius === 'farthest-side') {
r = Math.max(cx - bLeft, cy - bTop, bLeft + bWidth - cx, bTop + bHeight - cy);
}
else {
// Per CSS spec, percentage is relative to sqrt(w² + h²) / sqrt(2).
r = (0, length_percentage_1.getAbsoluteValue)(clipPath.radius, Math.sqrt(bWidth * bWidth + bHeight * bHeight) / Math.SQRT2);
}
return new effects_1.ClipPathEffect((ctx) => {
ctx.beginPath();
ctx.arc(cx, cy, Math.max(0, r), 0, Math.PI * 2);
ctx.clip();
});
}
case 3 /* CLIP_PATH_TYPE.ELLIPSE */: {
const cx = bLeft + (0, length_percentage_1.getAbsoluteValue)(clipPath.cx, bWidth);
const cy = bTop + (0, length_percentage_1.getAbsoluteValue)(clipPath.cy, bHeight);
const rx = resolveAxisRadius(clipPath.rx, cx, bLeft, bLeft + bWidth, bWidth);
const ry = resolveAxisRadius(clipPath.ry, cy, bTop, bTop + bHeight, bHeight);
return new effects_1.ClipPathEffect((ctx) => {
ctx.beginPath();
ctx.ellipse(cx, cy, Math.max(0, rx), Math.max(0, ry), 0, 0, Math.PI * 2);
ctx.clip();
});
}
case 4 /* CLIP_PATH_TYPE.POLYGON */: {
// Pre-compute all vertices in page-absolute coordinates.
const absPoints = clipPath.points.map(([px, py]) => [bLeft + (0, length_percentage_1.getAbsoluteValue)(px, bWidth), bTop + (0, length_percentage_1.getAbsoluteValue)(py, bHeight)]);
return new effects_1.ClipPathEffect((ctx) => {
ctx.beginPath();
if (absPoints.length > 0) {
ctx.moveTo(absPoints[0][0], absPoints[0][1]);
for (let i = 1; i < absPoints.length; i++) {
ctx.lineTo(absPoints[i][0], absPoints[i][1]);
}
ctx.closePath();
}
// Calling clip() with an empty path (zero points) is intentional:
// it clips the entire region to nothing, which is the correct
// behaviour for a degenerate polygon() per the CSS spec.
ctx.clip();
});
}
case 5 /* CLIP_PATH_TYPE.PATH */: {
// path() coordinates are in the element's local space (0,0 = element top-left).
// We temporarily translate the canvas origin to the element's position, clip
// with the Path2D, then restore only the transform matrix (not the clipping
// region) via setTransform so the clip persists for the enclosing
// ctx.save() / ctx.restore() pair managed by EffectsRenderer.
//
// When the element also has a CSS transform, that transform was already applied
// by a preceding TransformEffect, so the path coordinates end up correctly in
// the element's transformed local space — matching browser behaviour.
const { d } = clipPath;
return new effects_1.ClipPathEffect((ctx) => {
try {
const savedTransform = ctx.getTransform();
ctx.translate(bLeft, bTop);
ctx.clip(new Path2D(d));
ctx.setTransform(savedTransform);
}
catch (_e) {
// Path2D or getTransform/setTransform not supported in this environment.
}
});
}
case 0 /* CLIP_PATH_TYPE.NONE */:
return null;
default: {
// Exhaustiveness guard: if a new CLIP_PATH_TYPE is added in the future
// without a corresponding case above, TypeScript will raise a compile-time
// error here rather than silently falling through.
const _exhaustive = clipPath;
void _exhaustive;
return null;
}
}
};
const parseStackTree = (parent, stackingContext, realStackingContext, listItems) => {
parent.container.elements.forEach((child) => {
const treatAsRealStackingContext = (0, bitwise_1.contains)(child.flags, 4 /* FLAGS.CREATES_REAL_STACKING_CONTEXT */);
const createsStackingContext = (0, bitwise_1.contains)(child.flags, 2 /* FLAGS.CREATES_STACKING_CONTEXT */);
const paintContainer = new ElementPaint(child, parent);
if ((0, bitwise_1.contains)(child.styles.display, 2048 /* DISPLAY.LIST_ITEM */)) {
listItems.push(paintContainer);
}
const listOwnerItems = (0, bitwise_1.contains)(child.flags, 8 /* FLAGS.IS_LIST_OWNER */) ? [] : listItems;
if (treatAsRealStackingContext || createsStackingContext) {
const parentStack = treatAsRealStackingContext || child.styles.isPositioned() ? realStackingContext : stackingContext;
const stack = new StackingContext(paintContainer);
if (child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
const order = child.styles.zIndex.order;
if (order < 0) {
let index = 0;
parentStack.negativeZIndex.some((current, i) => {
if (order > current.element.container.styles.zIndex.order) {
index = i;
return false;
}
else if (index > 0) {
return true;
}
return false;
});
parentStack.negativeZIndex.splice(index, 0, stack);
}
else if (order > 0) {
let index = 0;
parentStack.positiveZIndex.some((current, i) => {
if (order >= current.element.container.styles.zIndex.order) {
index = i + 1;
return false;
}
else if (index > 0) {
return true;
}
return false;
});
parentStack.positiveZIndex.splice(index, 0, stack);
}
else {
parentStack.zeroOrAutoZIndexOrTransformedOrOpacity.push(stack);
}
}
else {
if (child.styles.isFloating()) {
parentStack.nonPositionedFloats.push(stack);
}
else {
parentStack.nonPositionedInlineLevel.push(stack);
}
}
parseStackTree(paintContainer, stack, treatAsRealStackingContext ? stack : realStackingContext, listOwnerItems);
}
else {
if (child.styles.isInlineLevel()) {
stackingContext.inlineLevel.push(paintContainer);
}
else {
stackingContext.nonInlineLevel.push(paintContainer);
}
parseStackTree(paintContainer, stackingContext, realStackingContext, listOwnerItems);
}
if ((0, bitwise_1.contains)(child.flags, 8 /* FLAGS.IS_LIST_OWNER */)) {
processListItems(child, listOwnerItems);
}
});
};
const processListItems = (owner, elements) => {
let numbering = owner instanceof ol_element_container_1.OLElementContainer ? owner.start : 1;
const reversed = owner instanceof ol_element_container_1.OLElementContainer ? owner.reversed : false;
for (let i = 0; i < elements.length; i++) {
const item = elements[i];
if (item.container instanceof li_element_container_1.LIElementContainer &&
typeof item.container.value === 'number' &&
item.container.value !== 0) {
numbering = item.container.value;
}
item.listValue = (0, counter_1.createCounterText)(numbering, item.container.styles.listStyleType, true);
numbering += reversed ? -1 : 1;
}
};
const parseStackingContexts = (container) => {
const paintContainer = new ElementPaint(container, null);
const root = new StackingContext(paintContainer);
const listItems = [];
parseStackTree(paintContainer, root, root, listItems);
processListItems(paintContainer.container, listItems);
return root;
};
exports.parseStackingContexts = parseStackingContexts;
//# sourceMappingURL=stacking-context.js.map