mirror of
https://github.com/excalidraw/excalidraw.git
synced 2026-05-19 04:15:02 -04:00
@@ -146,6 +146,13 @@ export interface ExcalidrawElementWithCanvas {
|
||||
boundTextCanvas: HTMLCanvasElement;
|
||||
canvasOriginSceneX?: number;
|
||||
canvasOriginSceneY?: number;
|
||||
/**
|
||||
* Tip canvas for incremental freedraw rendering. Contains only the last
|
||||
* unfinalised segment (whose Catmull-Rom right-hand tangent changes with
|
||||
* each new point) and is cleared + redrawn every frame. Composited on top
|
||||
* of `canvas` (the committed accumulation canvas) in drawElementFromCanvas.
|
||||
*/
|
||||
tipCanvas?: HTMLCanvasElement;
|
||||
}
|
||||
|
||||
const cappedElementCanvasSize = (
|
||||
@@ -410,7 +417,7 @@ const drawTaperedCapsule = (
|
||||
const r = Math.max(r0, r1);
|
||||
|
||||
if (len < r / 2) {
|
||||
// Degenerate segment — draw a filled circle at the larger radius
|
||||
// Degenerate segment - draw a filled circle at the larger radius
|
||||
context.beginPath();
|
||||
context.arc((x0 + x1) / 2, (y0 + y1) / 2, r, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
@@ -424,11 +431,11 @@ const drawTaperedCapsule = (
|
||||
context.beginPath();
|
||||
// Back semicircle at P0: clockwise from (P0 + perp*r0) through (back of P0) to (P0 - perp*r0)
|
||||
context.arc(x0, y0, r0, angle + Math.PI / 2, angle - Math.PI / 2, false);
|
||||
// Neg-perp side: P0 - perp*r0 → P1 - perp*r1 (arc endpoint is already P0 - perp*r0)
|
||||
// Neg-perp side: P0 - perp*r0 -> P1 - perp*r1 (arc endpoint is already P0 - perp*r0)
|
||||
context.lineTo(x1 - px * r1, y1 - py * r1);
|
||||
// Front semicircle at P1: clockwise from (P1 - perp*r1) through (front of P1) to (P1 + perp*r1)
|
||||
context.arc(x1, y1, r1, angle - Math.PI / 2, angle + Math.PI / 2, false);
|
||||
// Perp side: P1 + perp*r1 → P0 + perp*r0
|
||||
// Perp side: P1 + perp*r1 -> P0 + perp*r0
|
||||
context.lineTo(x0 + px * r0, y0 + py * r0);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
@@ -453,14 +460,11 @@ const PRESSURE_SMOOTHING_RADIUS = 6;
|
||||
/**
|
||||
* Returns the Catmull-Rom tangent vector at points[i], using the neighbouring
|
||||
* points for a uniform parameterisation. At the first point a one-sided
|
||||
* forward tangent is used. At the last real point, `predictedPoint` (if
|
||||
* supplied) stands in for the missing next neighbour so the stroke tip curves
|
||||
* smoothly toward the predicted pen position.
|
||||
* forward tangent is used.
|
||||
*/
|
||||
const getCatmullRomTangent = (
|
||||
points: readonly (readonly [number, number])[],
|
||||
i: number,
|
||||
predictedPoint: readonly [number, number] | undefined,
|
||||
): [number, number] => {
|
||||
const N = points.length;
|
||||
const cur = points[i];
|
||||
@@ -469,8 +473,6 @@ const getCatmullRomTangent = (
|
||||
let next: readonly [number, number];
|
||||
if (i < N - 1) {
|
||||
next = points[i + 1];
|
||||
} else if (predictedPoint !== undefined) {
|
||||
next = predictedPoint;
|
||||
} else {
|
||||
// Mirror back across cur to get a forward tangent at the last point.
|
||||
const prev2 = i > 0 ? points[i - 1] : cur;
|
||||
@@ -576,21 +578,23 @@ const drawSubdividedSegment = (
|
||||
* Draws freedraw points as bezier-subdivided, pressure-aware tapered capsule
|
||||
* segments. Consecutive real points are connected with Catmull-Rom cubic
|
||||
* bezier curves so the rendered stroke is smooth even when input samples are
|
||||
* sparse. When `predictedPoint` is supplied it is used as the tangent hint at
|
||||
* the tip of the stroke and an additional ghost segment is drawn toward it,
|
||||
* compensating for pointer-event latency.
|
||||
* sparse.
|
||||
*
|
||||
* @param fromIndex Draw segments starting from this index (inclusive).
|
||||
* Pass 0 to draw everything.
|
||||
* @param predictedPoint Element-local scene coords of the first predicted
|
||||
* pointer event, used for tangent and ghost rendering.
|
||||
* @param fromIndex Draw segments starting from this point index (inclusive).
|
||||
* Pass 0 to draw from the beginning.
|
||||
* @param upToIndex Draw segments only up to (but not including) this point
|
||||
* index. Omit or pass `undefined` to draw all remaining
|
||||
* points. Used by the incremental canvas to stop short of
|
||||
* the last segment so the committed canvas only contains
|
||||
* segments whose Catmull-Rom tangents are fully finalised
|
||||
* (i.e. the right-hand neighbour is known).
|
||||
*/
|
||||
const drawFreeDrawSegments = (
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
context: CanvasRenderingContext2D,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
fromIndex: number,
|
||||
predictedPoint?: readonly [number, number],
|
||||
upToIndex?: number,
|
||||
) => {
|
||||
const { points, pressures } = element;
|
||||
const N = points.length;
|
||||
@@ -628,23 +632,23 @@ const drawFreeDrawSegments = (
|
||||
return totalWeight > 0 ? sum / totalWeight : DEFAULT_FREEDRAW_PRESSURE;
|
||||
};
|
||||
|
||||
if (fromIndex === 0 && N === 1) {
|
||||
// Single-point stroke → filled circle (dot)
|
||||
if (
|
||||
fromIndex === 0 &&
|
||||
N === 1 &&
|
||||
(upToIndex === undefined || upToIndex >= 1)
|
||||
) {
|
||||
// Single-point stroke -> filled circle (dot)
|
||||
const r = baseRadius * getSmoothedPressure(0) * 2;
|
||||
context.beginPath();
|
||||
context.arc(points[0][0], points[0][1], r, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
// Draw ghost circle at predicted point if available
|
||||
if (predictedPoint !== undefined) {
|
||||
context.beginPath();
|
||||
context.arc(predictedPoint[0], predictedPoint[1], r, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const end = upToIndex !== undefined ? Math.min(upToIndex, N) : N;
|
||||
const start = Math.max(fromIndex, 1);
|
||||
for (let i = start; i < N; i++) {
|
||||
for (let i = start; i < end; i++) {
|
||||
const p0 = points[i - 1];
|
||||
const p1 = points[i];
|
||||
// Very first pressure values are often unreliable,
|
||||
@@ -652,15 +656,8 @@ const drawFreeDrawSegments = (
|
||||
const r0 = baseRadius * getSmoothedPressure(i - 1) * 2;
|
||||
const r1 = baseRadius * getSmoothedPressure(i) * 2;
|
||||
|
||||
// Catmull-Rom tangents. At the last real point, use predictedPoint as
|
||||
// the look-ahead so the tip curves smoothly toward the expected position.
|
||||
const isLastPoint = i === N - 1;
|
||||
const t0 = getCatmullRomTangent(points, i - 1, undefined);
|
||||
const t1 = getCatmullRomTangent(
|
||||
points,
|
||||
i,
|
||||
isLastPoint ? predictedPoint : undefined,
|
||||
);
|
||||
const t0 = getCatmullRomTangent(points, i - 1);
|
||||
const t1 = getCatmullRomTangent(points, i);
|
||||
|
||||
drawSubdividedSegment(
|
||||
context,
|
||||
@@ -676,34 +673,6 @@ const drawFreeDrawSegments = (
|
||||
t1[1],
|
||||
);
|
||||
}
|
||||
|
||||
// Ghost segment: extend the visible stroke toward the predicted point to
|
||||
// compensate for pointer-event latency. This segment is overwritten when
|
||||
// the next real pointer event arrives.
|
||||
if (predictedPoint !== undefined && N >= 1) {
|
||||
const lastPt = points[N - 1];
|
||||
const r0 = baseRadius * getSmoothedPressure(N - 1) * 2;
|
||||
|
||||
// Tangent at the last real point (with predicted look-ahead)
|
||||
const t0 = getCatmullRomTangent(points, N - 1, predictedPoint);
|
||||
// Forward tangent at the predicted point itself
|
||||
const fwdX = predictedPoint[0] - lastPt[0];
|
||||
const fwdY = predictedPoint[1] - lastPt[1];
|
||||
|
||||
drawSubdividedSegment(
|
||||
context,
|
||||
lastPt[0],
|
||||
lastPt[1],
|
||||
r0,
|
||||
predictedPoint[0],
|
||||
predictedPoint[1],
|
||||
r0, // hold pressure at the tip
|
||||
t0[0],
|
||||
t0[1],
|
||||
fwdX,
|
||||
fwdY,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const drawElementOnCanvas = (
|
||||
@@ -917,7 +886,7 @@ export const elementWithCanvasCache = new WeakMap<
|
||||
// accumulates new capsule segments without full regeneration on every added
|
||||
// point.
|
||||
|
||||
// screen pixels — minimum extra lookahead space on each side
|
||||
// screen pixels - minimum extra lookahead space on each side
|
||||
// (divided by scale at use)
|
||||
const FREEDRAW_CANVAS_OVERSHOOT_MIN = 200;
|
||||
|
||||
@@ -925,8 +894,31 @@ const FREEDRAW_CANVAS_OVERSHOOT_MIN = 200;
|
||||
const FREEDRAW_CANVAS_OVERSHOOT_FACTOR = 0.5;
|
||||
|
||||
interface FreeDrawIncrementalCanvas {
|
||||
canvas: HTMLCanvasElement;
|
||||
lastRenderedPointCount: number;
|
||||
/**
|
||||
* Accumulation canvas - contains all segments whose Catmull-Rom tangents are
|
||||
* fully finalised (right-hand neighbour is known). With N points the last
|
||||
* finalised segment ends at index `committedPointCount - 1`, meaning segment
|
||||
* `[committedPointCount-2 -> committedPointCount-1]` has been drawn with the
|
||||
* correct tangent at `committedPointCount-1` (since point
|
||||
* `committedPointCount` existed when it was drawn). Never cleared; only
|
||||
* appended to (or copied when bounds grow).
|
||||
*/
|
||||
committedCanvas: HTMLCanvasElement;
|
||||
/**
|
||||
* Tip canvas - same pixel dimensions and scene origin as `committedCanvas`.
|
||||
* Cleared and redrawn every frame to contain only the last segment
|
||||
* `[committedPointCount-1 -> N-1]` whose tangent at `N-1` is still
|
||||
* provisional (no right-hand neighbour yet). Composited on top of
|
||||
* `committedCanvas` at display time.
|
||||
*/
|
||||
tipCanvas: HTMLCanvasElement;
|
||||
/**
|
||||
* Number of points that have been permanently drawn on `committedCanvas`.
|
||||
* The committed canvas contains segments through point index
|
||||
* `committedPointCount - 1` with final tangents. Always lags the current
|
||||
* point count by 1 (the tip holds the last unfinalisable segment).
|
||||
*/
|
||||
committedPointCount: number;
|
||||
canvasOriginSceneX: number;
|
||||
canvasOriginSceneY: number;
|
||||
canvasAllocX1: number;
|
||||
@@ -943,13 +935,26 @@ const freedrawIncrementalCache = new WeakMap<
|
||||
>();
|
||||
|
||||
/**
|
||||
* Generates or incrementally updates a raster canvas for a freedraw element
|
||||
* that is being actively drawn. Unlike the standard element canvas cache, this
|
||||
* cache is NOT cleared on each mutateElement call, allowing new segments to be
|
||||
* appended without full regeneration.
|
||||
* Generates or incrementally updates the two-canvas (committed + tip) raster
|
||||
* for a freedraw element being actively drawn.
|
||||
*
|
||||
* When element bounds grow beyond the over-allocated canvas, the existing
|
||||
* raster is copied into a new larger canvas before appending the next segments
|
||||
* ## Two-canvas split
|
||||
*
|
||||
* A Catmull-Rom tangent at point `i` depends on `points[i+1]`. Until
|
||||
* `points[i+1]` arrives, the tangent at `i` uses a mirrored fallback and is
|
||||
* therefore provisional. The segment ending at the current tip `[N-2 -> N-1]`
|
||||
* is the only one with a provisional tangent.
|
||||
*
|
||||
* - **`committedCanvas`** - contains all segments whose tangents are final.
|
||||
* With N points: segments `[0->1, ..., N-3->N-2]` (`committedPointCount =
|
||||
* N-1`). This canvas is append-only; its pixels are never invalidated.
|
||||
* When a new point `N` arrives, the segment `[N-2 -> N-1]` is now
|
||||
* finalised (tangent at `N-1` uses `N` as the right-hand neighbour) and is
|
||||
* drawn onto the committed canvas. `committedPointCount` advances to `N`.
|
||||
*
|
||||
* - **`tipCanvas`** - cleared and redrawn every frame to contain only the
|
||||
* last provisional segment `[committedPointCount-1 -> N-1]`. Composited on
|
||||
* top of `committedCanvas` at display time.
|
||||
*/
|
||||
const generateOrUpdateFreeDrawIncrementalCanvas = (
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
@@ -964,6 +969,7 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const containingFrameOpacity =
|
||||
getContainingFrame(element, elementsMap)?.opacity || 100;
|
||||
const N = element.points.length;
|
||||
|
||||
const prevInc = freedrawIncrementalCache.get(element);
|
||||
|
||||
@@ -980,22 +986,26 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
|
||||
prevInc.scale !== scale ||
|
||||
prevInc.theme !== appState.theme;
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
// ── Canvas allocation / reallocation ──────────────────────────────────────
|
||||
let committedCanvas: HTMLCanvasElement;
|
||||
let tipCanvas: HTMLCanvasElement;
|
||||
let canvasOriginSceneX: number;
|
||||
let canvasOriginSceneY: number;
|
||||
let fromIndex: number;
|
||||
let canvasScale: number;
|
||||
// How many points to start the committed-canvas update from. On a full
|
||||
// regen this is 0; on a bounds-exceeded realloc it is the existing committed
|
||||
// count so we only append the new segments.
|
||||
let committedFromIndex: number;
|
||||
|
||||
if (needsAlloc) {
|
||||
// Over-allocate proportionally to the current bounding box, like std::vector doubling,
|
||||
// so fast large strokes trigger far fewer reallocations. The over-sized canvas is
|
||||
// discarded on stroke finalisation so the memory waste is only transient.
|
||||
// Over-allocate proportionally to the current bounding box so fast large
|
||||
// strokes trigger far fewer reallocations.
|
||||
const overshootX = Math.max(
|
||||
FREEDRAW_CANVAS_OVERSHOOT_MIN / scale, // convert screen-pixel budget to scene units
|
||||
FREEDRAW_CANVAS_OVERSHOOT_MIN / scale,
|
||||
(x2 - x1) * FREEDRAW_CANVAS_OVERSHOOT_FACTOR,
|
||||
);
|
||||
const overshootY = Math.max(
|
||||
FREEDRAW_CANVAS_OVERSHOOT_MIN / scale, // convert screen-pixel budget to scene units
|
||||
FREEDRAW_CANVAS_OVERSHOOT_MIN / scale,
|
||||
(y2 - y1) * FREEDRAW_CANVAS_OVERSHOOT_FACTOR,
|
||||
);
|
||||
const allocX1 = x1 - overshootX;
|
||||
@@ -1006,11 +1016,10 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
|
||||
canvasOriginSceneX = allocX1 - padding / dpr;
|
||||
canvasOriginSceneY = allocY1 - padding / dpr;
|
||||
|
||||
// Raw canvas pixels before zoom scale
|
||||
const rawW = (allocX2 - allocX1) * dpr + padding * 2;
|
||||
const rawH = (allocY2 - allocY1) * dpr + padding * 2;
|
||||
|
||||
// Respect browser canvas size limits
|
||||
// Respect browser canvas size limits.
|
||||
const AREA_LIMIT = 16777216;
|
||||
const WIDTH_HEIGHT_LIMIT = 32767;
|
||||
canvasScale = scale;
|
||||
@@ -1033,10 +1042,13 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
|
||||
return null;
|
||||
}
|
||||
|
||||
canvas = document.createElement("canvas");
|
||||
canvas.width = canvasWidth;
|
||||
canvas.height = canvasHeight;
|
||||
const context = canvas.getContext("2d")!;
|
||||
committedCanvas = document.createElement("canvas");
|
||||
committedCanvas.width = canvasWidth;
|
||||
committedCanvas.height = canvasHeight;
|
||||
|
||||
tipCanvas = document.createElement("canvas");
|
||||
tipCanvas.width = canvasWidth;
|
||||
tipCanvas.height = canvasHeight;
|
||||
|
||||
if (
|
||||
prevInc !== undefined &&
|
||||
@@ -1044,22 +1056,25 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
|
||||
prevInc.scale === canvasScale &&
|
||||
prevInc.theme === appState.theme
|
||||
) {
|
||||
// Copy existing raster to the new canvas at the correct pixel offset.
|
||||
// The shift is determined by how much the canvas origin moved in scene coords.
|
||||
// Bounds grew: copy committed raster to new canvas at the correct offset
|
||||
// and keep accumulating. Tip will be redrawn below.
|
||||
const copyX =
|
||||
(prevInc.canvasOriginSceneX - canvasOriginSceneX) * dpr * canvasScale;
|
||||
const copyY =
|
||||
(prevInc.canvasOriginSceneY - canvasOriginSceneY) * dpr * canvasScale;
|
||||
context.drawImage(prevInc.canvas, copyX, copyY);
|
||||
fromIndex = prevInc.lastRenderedPointCount;
|
||||
committedCanvas
|
||||
.getContext("2d")!
|
||||
.drawImage(prevInc.committedCanvas, copyX, copyY);
|
||||
committedFromIndex = prevInc.committedPointCount - 1;
|
||||
} else {
|
||||
// Full regeneration on first canvas, zoom change, or theme change
|
||||
fromIndex = 0;
|
||||
// Full regeneration: zoom/theme change or first frame.
|
||||
committedFromIndex = 0;
|
||||
}
|
||||
|
||||
freedrawIncrementalCache.set(element, {
|
||||
canvas,
|
||||
lastRenderedPointCount: fromIndex,
|
||||
committedCanvas,
|
||||
tipCanvas,
|
||||
committedPointCount: committedFromIndex,
|
||||
canvasOriginSceneX,
|
||||
canvasOriginSceneY,
|
||||
canvasAllocX1: allocX1,
|
||||
@@ -1070,49 +1085,73 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
|
||||
theme: appState.theme,
|
||||
});
|
||||
} else {
|
||||
canvas = prevInc.canvas;
|
||||
committedCanvas = prevInc.committedCanvas;
|
||||
tipCanvas = prevInc.tipCanvas;
|
||||
canvasOriginSceneX = prevInc.canvasOriginSceneX;
|
||||
canvasOriginSceneY = prevInc.canvasOriginSceneY;
|
||||
fromIndex = prevInc.lastRenderedPointCount;
|
||||
canvasScale = prevInc.scale;
|
||||
committedFromIndex = prevInc.committedPointCount - 1;
|
||||
}
|
||||
|
||||
// Roll back 2 points so the Catmull-Rom look-ahead tangent at the last
|
||||
// committed segment is redrawn correctly when a new point arrives.
|
||||
// Pressure smoothing is causal (one-sided) so it requires no extra rollback.
|
||||
// Overpainting with an opaque fill is harmless.
|
||||
const drawFrom = Math.max(0, fromIndex - 2);
|
||||
const inc = freedrawIncrementalCache.get(element)!;
|
||||
|
||||
// Draw new (and possibly revised) segments plus the ghost toward the
|
||||
// predicted point.
|
||||
if (drawFrom < element.points.length) {
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const canvasElemOffsetX =
|
||||
(element.x - canvasOriginSceneX) * dpr * canvasScale;
|
||||
const canvasElemOffsetY =
|
||||
(element.y - canvasOriginSceneY) * dpr * canvasScale;
|
||||
// ── Helper: draw onto a canvas with the element's scene->pixel transform ──
|
||||
const withElementContext = (
|
||||
target: HTMLCanvasElement,
|
||||
fn: (ctx: CanvasRenderingContext2D) => void,
|
||||
) => {
|
||||
const ctx = target.getContext("2d")!;
|
||||
const offsetX = (element.x - canvasOriginSceneX) * dpr * canvasScale;
|
||||
const offsetY = (element.y - canvasOriginSceneY) * dpr * canvasScale;
|
||||
ctx.save();
|
||||
ctx.translate(canvasElemOffsetX, canvasElemOffsetY);
|
||||
ctx.translate(offsetX, offsetY);
|
||||
ctx.scale(dpr * canvasScale, dpr * canvasScale);
|
||||
fn(ctx);
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
// ── Update committed canvas ───────────────────────────────────────────────
|
||||
// With N points the last finalisable segment ends at N-2 (needs N-1 as
|
||||
// right-hand neighbour for the tangent at N-2, and N-1 is always present).
|
||||
// We draw from `committedFromIndex` up to (but not including) point N-1,
|
||||
// so the committed canvas contains segments [0->1, ..., N-3->N-2].
|
||||
const newCommittedCount = Math.max(1, N - 1);
|
||||
if (committedFromIndex < newCommittedCount) {
|
||||
withElementContext(committedCanvas, (ctx) => {
|
||||
drawFreeDrawSegments(
|
||||
element,
|
||||
ctx,
|
||||
renderConfig,
|
||||
committedFromIndex,
|
||||
newCommittedCount, // upToIndex - stop before the last provisional segment
|
||||
);
|
||||
});
|
||||
inc.committedPointCount = newCommittedCount;
|
||||
}
|
||||
|
||||
// ── Redraw tip canvas ─────────────────────────────────────────────────────
|
||||
// Always cleared and redrawn: contains the single provisional segment
|
||||
// [committedPointCount-1 -> N-1] with a predicted-point ghost if available.
|
||||
withElementContext(tipCanvas, (ctx) => {
|
||||
ctx.clearRect(
|
||||
-(element.x - canvasOriginSceneX),
|
||||
-(element.y - canvasOriginSceneY),
|
||||
tipCanvas.width / (dpr * canvasScale),
|
||||
tipCanvas.height / (dpr * canvasScale),
|
||||
);
|
||||
drawFreeDrawSegments(
|
||||
element,
|
||||
ctx,
|
||||
renderConfig,
|
||||
drawFrom,
|
||||
element.points[element.points.length - 1],
|
||||
//predictedPoint,
|
||||
inc.committedPointCount - 1,
|
||||
undefined, // draw to natural end (the tip segment)
|
||||
);
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
// Update lastRenderedPointCount in the cache entry.
|
||||
const inc = freedrawIncrementalCache.get(element)!;
|
||||
inc.lastRenderedPointCount = element.points.length;
|
||||
}
|
||||
|
||||
const inc = freedrawIncrementalCache.get(element)!;
|
||||
return {
|
||||
element,
|
||||
canvas,
|
||||
canvas: committedCanvas,
|
||||
tipCanvas,
|
||||
theme: appState.theme,
|
||||
scale: canvasScale,
|
||||
angle: element.angle,
|
||||
@@ -1279,6 +1318,19 @@ const drawElementFromCanvas = (
|
||||
elementWithCanvas.canvas!.height / elementWithCanvas.scale,
|
||||
);
|
||||
|
||||
// Composite the tip canvas (incremental freedraw path) on top. It is
|
||||
// the same size and at the same scene origin as the committed canvas, so
|
||||
// it uses identical destX / destY / dimensions.
|
||||
if (elementWithCanvas.tipCanvas) {
|
||||
context.drawImage(
|
||||
elementWithCanvas.tipCanvas,
|
||||
destX,
|
||||
destY,
|
||||
elementWithCanvas.tipCanvas.width / elementWithCanvas.scale,
|
||||
elementWithCanvas.tipCanvas.height / elementWithCanvas.scale,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
import.meta.env.VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
|
||||
"true" &&
|
||||
@@ -1555,7 +1607,7 @@ export const renderElement = (
|
||||
}
|
||||
|
||||
context.restore();
|
||||
// not exporting → optimized rendering (cache & render from element
|
||||
// not exporting -> optimized rendering (cache & render from element
|
||||
// canvases)
|
||||
} else {
|
||||
const elementWithCanvas = generateElementWithCanvas(
|
||||
|
||||
Reference in New Issue
Block a user