blob: 279eabac35993c4b37329360693ef29f35564a24 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {type Bounds, type PathCommands, type Position} from './common.js';
import {drawPath, emptyBounds, type LineStyle, type PathBounds} from './highlight_common.js';
type SnapAlignment = 'none'|'start'|'end'|'center';
export interface ScrollSnapHighlight {
snapport: PathCommands;
paddingBox: PathCommands;
snapAreas: Array<{
path: PathCommands,
borderBox: PathCommands,
alignBlock?: SnapAlignment,
alignInline?: SnapAlignment,
}>;
snapportBorder: LineStyle;
snapAreaBorder: LineStyle;
scrollMarginColor: string;
scrollPaddingColor: string;
}
function getSnapAlignBlockPoint(bounds: Bounds, align: SnapAlignment): Position|undefined {
if (align === 'start') {
return {
x: (bounds.minX + bounds.maxX) / 2,
y: bounds.minY,
};
}
if (align === 'center') {
return {
x: (bounds.minX + bounds.maxX) / 2,
y: (bounds.minY + bounds.maxY) / 2,
};
}
if (align === 'end') {
return {
x: (bounds.minX + bounds.maxX) / 2,
y: bounds.maxY,
};
}
return;
}
function getSnapAlignInlinePoint(bounds: Bounds, align: SnapAlignment): Position|undefined {
if (align === 'start') {
return {
x: bounds.minX,
y: (bounds.minY + bounds.maxY) / 2,
};
}
if (align === 'center') {
return {
x: (bounds.minX + bounds.maxX) / 2,
y: (bounds.minY + bounds.maxY) / 2,
};
}
if (align === 'end') {
return {
x: bounds.maxX,
y: (bounds.minY + bounds.maxY) / 2,
};
}
return;
}
const ALIGNMENT_POINT_STROKE_WIDTH = 5;
const ALIGNMENT_POINT_STROKE_COLOR = 'white';
const ALIGNMENT_POINT_OUTER_RADIUS = 6;
const ALIGNMENT_POINT_FILL_COLOR = '#4585f6';
const ALIGNMENT_POINT_INNER_RADIUS = 4;
function drawAlignment(context: CanvasRenderingContext2D, point: Position, bounds: Bounds): void {
let startAngle = 0;
let renderFullCircle = true;
if (point.x === bounds.minX) {
startAngle = -0.5 * Math.PI;
renderFullCircle = false;
} else if (point.x === bounds.maxX) {
startAngle = 0.5 * Math.PI;
renderFullCircle = false;
} else if (point.y === bounds.minY) {
startAngle = 0;
renderFullCircle = false;
} else if (point.y === bounds.maxY) {
startAngle = Math.PI;
renderFullCircle = false;
}
const endAngle = startAngle + (renderFullCircle ? 2 * Math.PI : Math.PI);
context.save();
context.beginPath();
context.lineWidth = ALIGNMENT_POINT_STROKE_WIDTH;
context.strokeStyle = ALIGNMENT_POINT_STROKE_COLOR;
context.arc(point.x, point.y, ALIGNMENT_POINT_OUTER_RADIUS, startAngle, endAngle);
context.stroke();
context.fillStyle = ALIGNMENT_POINT_FILL_COLOR;
context.arc(point.x, point.y, ALIGNMENT_POINT_INNER_RADIUS, startAngle, endAngle);
context.fill();
context.restore();
}
function drawScrollPadding(
highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number) {
drawPath(
context, highlight.paddingBox, highlight.scrollPaddingColor, undefined, undefined, emptyBounds(),
emulationScaleFactor);
// Clear the area so that previously rendered paddings remain.
context.save();
context.globalCompositeOperation = 'destination-out';
drawPath(context, highlight.snapport, 'white', undefined, undefined, emptyBounds(), emulationScaleFactor);
context.restore();
}
function drawSnapAreas(
highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number): PathBounds[] {
const bounds = [];
for (const area of highlight.snapAreas) {
const areaBounds = emptyBounds();
drawPath(
context, area.path, highlight.scrollMarginColor, highlight.snapAreaBorder.color,
highlight.snapAreaBorder.pattern, areaBounds, emulationScaleFactor);
// Clear the area so that previously rendered margins remain.
context.save();
context.globalCompositeOperation = 'destination-out';
drawPath(context, area.borderBox, 'white', undefined, undefined, emptyBounds(), emulationScaleFactor);
context.restore();
bounds.push(areaBounds);
}
return bounds;
}
function drawAlignmentPoints(
areaBounds: PathBounds[], highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D) {
for (let i = 0; i < highlight.snapAreas.length; i++) {
const area = highlight.snapAreas[i];
const inlinePoint = area.alignInline ? getSnapAlignInlinePoint(areaBounds[i], area.alignInline) : null;
const blockPoint = area.alignBlock ? getSnapAlignBlockPoint(areaBounds[i], area.alignBlock) : null;
if (inlinePoint) {
drawAlignment(context, inlinePoint, areaBounds[i]);
}
if (blockPoint) {
drawAlignment(context, blockPoint, areaBounds[i]);
}
}
}
function drawSnapportBorder(
highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number) {
drawPath(
context, highlight.snapport, undefined, highlight.snapportBorder.color, undefined, emptyBounds(),
emulationScaleFactor);
}
export function drawScrollSnapHighlight(
highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number) {
// The order of the following draw calls is important, change it carefully.
drawScrollPadding(highlight, context, emulationScaleFactor);
const areaBounds = drawSnapAreas(highlight, context, emulationScaleFactor);
drawSnapportBorder(highlight, context, emulationScaleFactor);
drawAlignmentPoints(areaBounds, highlight, context);
}