UNPKG

5.65 kBPlain TextView Raw
1
2// Copyright 2021 The Chromium Authors. All rights reserved.
3// Use of this source code is governed by a BSD-style license that can be
4// found in the LICENSE file.
5
6import type {Bounds, PathCommands, Position} from './common.js';
7import type {LineStyle, PathBounds} from './highlight_common.js';
8import {drawPath, emptyBounds} from './highlight_common.js';
9
10type SnapAlignment = 'none'|'start'|'end'|'center';
11export interface ScrollSnapHighlight {
12 snapport: PathCommands;
13 paddingBox: PathCommands;
14 snapAreas: Array<{
15 path: PathCommands,
16 borderBox: PathCommands,
17 alignBlock?: SnapAlignment,
18 alignInline?: SnapAlignment,
19 }>;
20 snapportBorder: LineStyle;
21 snapAreaBorder: LineStyle;
22 scrollMarginColor: string;
23 scrollPaddingColor: string;
24}
25
26function getSnapAlignBlockPoint(bounds: Bounds, align: SnapAlignment): Position|undefined {
27 if (align === 'start') {
28 return {
29 x: (bounds.minX + bounds.maxX) / 2,
30 y: bounds.minY,
31 };
32 }
33 if (align === 'center') {
34 return {
35 x: (bounds.minX + bounds.maxX) / 2,
36 y: (bounds.minY + bounds.maxY) / 2,
37 };
38 }
39 if (align === 'end') {
40 return {
41 x: (bounds.minX + bounds.maxX) / 2,
42 y: bounds.maxY,
43 };
44 }
45 return;
46}
47
48function getSnapAlignInlinePoint(bounds: Bounds, align: SnapAlignment): Position|undefined {
49 if (align === 'start') {
50 return {
51 x: bounds.minX,
52 y: (bounds.minY + bounds.maxY) / 2,
53 };
54 }
55 if (align === 'center') {
56 return {
57 x: (bounds.minX + bounds.maxX) / 2,
58 y: (bounds.minY + bounds.maxY) / 2,
59 };
60 }
61 if (align === 'end') {
62 return {
63 x: bounds.maxX,
64 y: (bounds.minY + bounds.maxY) / 2,
65 };
66 }
67 return;
68}
69
70const ALIGNMENT_POINT_STROKE_WIDTH = 5;
71const ALIGNMENT_POINT_STROKE_COLOR = 'white';
72const ALIGNMENT_POINT_OUTER_RADIUS = 6;
73const ALIGNMENT_POINT_FILL_COLOR = '#4585f6';
74const ALIGNMENT_POINT_INNER_RADIUS = 4;
75
76function drawAlignment(context: CanvasRenderingContext2D, point: Position, bounds: Bounds): void {
77 let startAngle = 0;
78 let renderFullCircle = true;
79 if (point.x === bounds.minX) {
80 startAngle = -0.5 * Math.PI;
81 renderFullCircle = false;
82 } else if (point.x === bounds.maxX) {
83 startAngle = 0.5 * Math.PI;
84 renderFullCircle = false;
85 } else if (point.y === bounds.minY) {
86 startAngle = 0;
87 renderFullCircle = false;
88 } else if (point.y === bounds.maxY) {
89 startAngle = Math.PI;
90 renderFullCircle = false;
91 }
92 const endAngle = startAngle + (renderFullCircle ? 2 * Math.PI : Math.PI);
93 context.save();
94 context.beginPath();
95 context.lineWidth = ALIGNMENT_POINT_STROKE_WIDTH;
96 context.strokeStyle = ALIGNMENT_POINT_STROKE_COLOR;
97 context.arc(point.x, point.y, ALIGNMENT_POINT_OUTER_RADIUS, startAngle, endAngle);
98 context.stroke();
99 context.fillStyle = ALIGNMENT_POINT_FILL_COLOR;
100 context.arc(point.x, point.y, ALIGNMENT_POINT_INNER_RADIUS, startAngle, endAngle);
101 context.fill();
102 context.restore();
103}
104
105function drawScrollPadding(
106 highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number) {
107 drawPath(
108 context, highlight.paddingBox, highlight.scrollPaddingColor, undefined, undefined, emptyBounds(),
109 emulationScaleFactor);
110
111 // Clear the area so that previously rendered paddings remain.
112 context.save();
113 context.globalCompositeOperation = 'destination-out';
114 drawPath(context, highlight.snapport, 'white', undefined, undefined, emptyBounds(), emulationScaleFactor);
115 context.restore();
116}
117
118function drawSnapAreas(
119 highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number): PathBounds[] {
120 const bounds = [];
121 for (const area of highlight.snapAreas) {
122 const areaBounds = emptyBounds();
123 drawPath(
124 context, area.path, highlight.scrollMarginColor, highlight.snapAreaBorder.color,
125 highlight.snapAreaBorder.pattern, areaBounds, emulationScaleFactor);
126
127 // Clear the area so that previously rendered margins remain.
128 context.save();
129 context.globalCompositeOperation = 'destination-out';
130 drawPath(context, area.borderBox, 'white', undefined, undefined, emptyBounds(), emulationScaleFactor);
131 context.restore();
132
133 bounds.push(areaBounds);
134 }
135 return bounds;
136}
137
138function drawAlignmentPoints(
139 areaBounds: PathBounds[], highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D) {
140 for (let i = 0; i < highlight.snapAreas.length; i++) {
141 const area = highlight.snapAreas[i];
142 const inlinePoint = area.alignInline ? getSnapAlignInlinePoint(areaBounds[i], area.alignInline) : null;
143 const blockPoint = area.alignBlock ? getSnapAlignBlockPoint(areaBounds[i], area.alignBlock) : null;
144 if (inlinePoint) {
145 drawAlignment(context, inlinePoint, areaBounds[i]);
146 }
147 if (blockPoint) {
148 drawAlignment(context, blockPoint, areaBounds[i]);
149 }
150 }
151}
152
153function drawSnapportBorder(
154 highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number) {
155 drawPath(
156 context, highlight.snapport, undefined, highlight.snapportBorder.color, undefined, emptyBounds(),
157 emulationScaleFactor);
158}
159
160export function drawScrollSnapHighlight(
161 highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number) {
162 // The order of the following draw calls is important, change it carefully.
163 drawScrollPadding(highlight, context, emulationScaleFactor);
164 const areaBounds = drawSnapAreas(highlight, context, emulationScaleFactor);
165 drawSnapportBorder(highlight, context, emulationScaleFactor);
166 drawAlignmentPoints(areaBounds, highlight, context);
167}