1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | import type {Bounds, PathCommands, Position} from './common.js';
|
7 | import type {LineStyle, PathBounds} from './highlight_common.js';
|
8 | import {drawPath, emptyBounds} from './highlight_common.js';
|
9 |
|
10 | type SnapAlignment = 'none'|'start'|'end'|'center';
|
11 | export 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 |
|
26 | function 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 |
|
48 | function 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 |
|
70 | const ALIGNMENT_POINT_STROKE_WIDTH = 5;
|
71 | const ALIGNMENT_POINT_STROKE_COLOR = 'white';
|
72 | const ALIGNMENT_POINT_OUTER_RADIUS = 6;
|
73 | const ALIGNMENT_POINT_FILL_COLOR = '#4585f6';
|
74 | const ALIGNMENT_POINT_INNER_RADIUS = 4;
|
75 |
|
76 | function 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 |
|
105 | function drawScrollPadding(
|
106 | highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number) {
|
107 | drawPath(
|
108 | context, highlight.paddingBox, highlight.scrollPaddingColor, undefined, undefined, emptyBounds(),
|
109 | emulationScaleFactor);
|
110 |
|
111 |
|
112 | context.save();
|
113 | context.globalCompositeOperation = 'destination-out';
|
114 | drawPath(context, highlight.snapport, 'white', undefined, undefined, emptyBounds(), emulationScaleFactor);
|
115 | context.restore();
|
116 | }
|
117 |
|
118 | function 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 |
|
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 |
|
138 | function 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 |
|
153 | function drawSnapportBorder(
|
154 | highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number) {
|
155 | drawPath(
|
156 | context, highlight.snapport, undefined, highlight.snapportBorder.color, undefined, emptyBounds(),
|
157 | emulationScaleFactor);
|
158 | }
|
159 |
|
160 | export function drawScrollSnapHighlight(
|
161 | highlight: ScrollSnapHighlight, context: CanvasRenderingContext2D, emulationScaleFactor: number) {
|
162 |
|
163 | drawScrollPadding(highlight, context, emulationScaleFactor);
|
164 | const areaBounds = drawSnapAreas(highlight, context, emulationScaleFactor);
|
165 | drawSnapportBorder(highlight, context, emulationScaleFactor);
|
166 | drawAlignmentPoints(areaBounds, highlight, context);
|
167 | }
|