1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | import type {AreaBounds, Bounds} from './common.js';
|
32 | import type {GridLabelState} from './css_grid_label_helpers.js';
|
33 | import {drawGridLabels} from './css_grid_label_helpers.js';
|
34 | import {applyMatrixToPoint, buildPath, emptyBounds, hatchFillPath} from './highlight_common.js';
|
35 |
|
36 |
|
37 | export const gridStyle = `
|
38 | /* Grid row and column labels */
|
39 | .grid-label-content {
|
40 | position: absolute;
|
41 | -webkit-user-select: none;
|
42 | padding: 2px;
|
43 | font-family: Menlo, monospace;
|
44 | font-size: 10px;
|
45 | min-width: 17px;
|
46 | min-height: 15px;
|
47 | border-radius: 2px;
|
48 | box-sizing: border-box;
|
49 | z-index: 1;
|
50 | background-clip: padding-box;
|
51 | pointer-events: none;
|
52 | text-align: center;
|
53 | display: flex;
|
54 | justify-content: center;
|
55 | align-items: center;
|
56 | }
|
57 |
|
58 | .grid-label-content[data-direction=row] {
|
59 | background-color: var(--row-label-color, #1A73E8);
|
60 | color: var(--row-label-text-color, #121212);
|
61 | }
|
62 |
|
63 | .grid-label-content[data-direction=column] {
|
64 | background-color: var(--column-label-color, #1A73E8);
|
65 | color: var(--column-label-text-color,#121212);
|
66 | }
|
67 |
|
68 | .line-names ul,
|
69 | .line-names .line-name {
|
70 | margin: 0;
|
71 | padding: 0;
|
72 | list-style: none;
|
73 | }
|
74 |
|
75 | .line-names .line-name {
|
76 | max-width: 100px;
|
77 | white-space: nowrap;
|
78 | overflow: hidden;
|
79 | text-overflow: ellipsis;
|
80 | }
|
81 |
|
82 | .line-names .grid-label-content,
|
83 | .line-numbers .grid-label-content,
|
84 | .track-sizes .grid-label-content {
|
85 | border: 1px solid white;
|
86 | --inner-corner-avoid-distance: 15px;
|
87 | }
|
88 |
|
89 | .grid-label-content.top-left.inner-shared-corner,
|
90 | .grid-label-content.top-right.inner-shared-corner {
|
91 | transform: translateY(var(--inner-corner-avoid-distance));
|
92 | }
|
93 |
|
94 | .grid-label-content.bottom-left.inner-shared-corner,
|
95 | .grid-label-content.bottom-right.inner-shared-corner {
|
96 | transform: translateY(calc(var(--inner-corner-avoid-distance) * -1));
|
97 | }
|
98 |
|
99 | .grid-label-content.left-top.inner-shared-corner,
|
100 | .grid-label-content.left-bottom.inner-shared-corner {
|
101 | transform: translateX(var(--inner-corner-avoid-distance));
|
102 | }
|
103 |
|
104 | .grid-label-content.right-top.inner-shared-corner,
|
105 | .grid-label-content.right-bottom.inner-shared-corner {
|
106 | transform: translateX(calc(var(--inner-corner-avoid-distance) * -1));
|
107 | }
|
108 |
|
109 | .line-names .grid-label-content::before,
|
110 | .line-numbers .grid-label-content::before,
|
111 | .track-sizes .grid-label-content::before {
|
112 | position: absolute;
|
113 | z-index: 1;
|
114 | pointer-events: none;
|
115 | content: "";
|
116 | width: 3px;
|
117 | height: 3px;
|
118 | border: 1px solid white;
|
119 | border-width: 0 1px 1px 0;
|
120 | }
|
121 |
|
122 | .line-names .grid-label-content[data-direction=row]::before,
|
123 | .line-numbers .grid-label-content[data-direction=row]::before,
|
124 | .track-sizes .grid-label-content[data-direction=row]::before {
|
125 | background: var(--row-label-color, #1A73E8);
|
126 | }
|
127 |
|
128 | .line-names .grid-label-content[data-direction=column]::before,
|
129 | .line-numbers .grid-label-content[data-direction=column]::before,
|
130 | .track-sizes .grid-label-content[data-direction=column]::before {
|
131 | background: var(--column-label-color, #1A73E8);
|
132 | }
|
133 |
|
134 | .grid-label-content.bottom-mid::before {
|
135 | transform: translateY(-1px) rotate(45deg);
|
136 | top: 100%;
|
137 | }
|
138 |
|
139 | .grid-label-content.top-mid::before {
|
140 | transform: translateY(-3px) rotate(-135deg);
|
141 | top: 0%;
|
142 | }
|
143 |
|
144 | .grid-label-content.left-mid::before {
|
145 | transform: translateX(-3px) rotate(135deg);
|
146 | left: 0%
|
147 | }
|
148 |
|
149 | .grid-label-content.right-mid::before {
|
150 | transform: translateX(3px) rotate(-45deg);
|
151 | right: 0%;
|
152 | }
|
153 |
|
154 | .grid-label-content.right-top::before {
|
155 | transform: translateX(3px) translateY(-1px) rotate(-90deg) skewY(30deg);
|
156 | right: 0%;
|
157 | top: 0%;
|
158 | }
|
159 |
|
160 | .grid-label-content.right-bottom::before {
|
161 | transform: translateX(3px) translateY(-3px) skewX(30deg);
|
162 | right: 0%;
|
163 | top: 100%;
|
164 | }
|
165 |
|
166 | .grid-label-content.bottom-right::before {
|
167 | transform: translateX(1px) translateY(-1px) skewY(30deg);
|
168 | right: 0%;
|
169 | top: 100%;
|
170 | }
|
171 |
|
172 | .grid-label-content.bottom-left::before {
|
173 | transform: translateX(-1px) translateY(-1px) rotate(90deg) skewX(30deg);
|
174 | left: 0%;
|
175 | top: 100%;
|
176 | }
|
177 |
|
178 | .grid-label-content.left-top::before {
|
179 | transform: translateX(-3px) translateY(-1px) rotate(180deg) skewX(30deg);
|
180 | left: 0%;
|
181 | top: 0%;
|
182 | }
|
183 |
|
184 | .grid-label-content.left-bottom::before {
|
185 | transform: translateX(-3px) translateY(-3px) rotate(90deg) skewY(30deg);
|
186 | left: 0%;
|
187 | top: 100%;
|
188 | }
|
189 |
|
190 | .grid-label-content.top-right::before {
|
191 | transform: translateX(1px) translateY(-3px) rotate(-90deg) skewX(30deg);
|
192 | right: 0%;
|
193 | top: 0%;
|
194 | }
|
195 |
|
196 | .grid-label-content.top-left::before {
|
197 | transform: translateX(-1px) translateY(-3px) rotate(180deg) skewY(30deg);
|
198 | left: 0%;
|
199 | top: 0%;
|
200 | }
|
201 |
|
202 | @media (forced-colors: active) {
|
203 | .grid-label-content {
|
204 | border-color: Highlight;
|
205 | background-color: Canvas;
|
206 | color: Text;
|
207 | forced-color-adjust: none;
|
208 | }
|
209 | .grid-label-content::before {
|
210 | background-color: Canvas;
|
211 | border-color: Highlight;
|
212 | }
|
213 | }`;
|
214 |
|
215 | export interface GridHighlight {
|
216 | gridBorder: Array<string|number>;
|
217 | writingMode: string;
|
218 | rowGaps: Array<string|number>;
|
219 | rotationAngle: number;
|
220 | columnGaps: Array<string|number>;
|
221 | rows: Array<string|number>;
|
222 | columns: Array<string|number>;
|
223 | areaNames: {[key: string]: Array<string|number>};
|
224 | gridHighlightConfig: {
|
225 | gridBackgroundColor?: string,
|
226 | gridBorderColor?: string,
|
227 | rowGapColor?: string,
|
228 | columnGapColor?: string,
|
229 | areaBorderColor?: string,
|
230 | gridBorderDash: boolean,
|
231 | rowLineDash: boolean,
|
232 | columnLineDash: boolean,
|
233 | showGridExtensionLines: boolean,
|
234 | showPositiveLineNumbers: boolean,
|
235 | showNegativeLineNumbers: boolean,
|
236 | rowLineColor: string,
|
237 | columnLineColor: string,
|
238 | rowHatchColor: string,
|
239 | columnHatchColor: string,
|
240 | showLineNames: boolean,
|
241 | };
|
242 | }
|
243 |
|
244 | export function drawLayoutGridHighlight(
|
245 | highlight: GridHighlight, context: CanvasRenderingContext2D, deviceScaleFactor: number, canvasWidth: number,
|
246 | canvasHeight: number, emulationScaleFactor: number, labelState: GridLabelState) {
|
247 | const gridBounds = emptyBounds();
|
248 | const gridPath = buildPath(highlight.gridBorder, gridBounds, emulationScaleFactor);
|
249 |
|
250 |
|
251 | context.save();
|
252 | applyWritingModeTransformation(highlight.writingMode, gridBounds, context);
|
253 |
|
254 |
|
255 | if (highlight.gridHighlightConfig.gridBackgroundColor) {
|
256 | context.fillStyle = highlight.gridHighlightConfig.gridBackgroundColor;
|
257 | context.fill(gridPath);
|
258 | }
|
259 |
|
260 |
|
261 | if (highlight.gridHighlightConfig.gridBorderColor) {
|
262 | context.save();
|
263 | context.translate(0.5, 0.5);
|
264 | context.lineWidth = 0;
|
265 | if (highlight.gridHighlightConfig.gridBorderDash) {
|
266 | context.setLineDash([3, 3]);
|
267 | }
|
268 | context.strokeStyle = highlight.gridHighlightConfig.gridBorderColor;
|
269 | context.stroke(gridPath);
|
270 | context.restore();
|
271 | }
|
272 |
|
273 |
|
274 | const rowBounds = drawGridLines(context, highlight, 'row', emulationScaleFactor);
|
275 | const columnBounds = drawGridLines(context, highlight, 'column', emulationScaleFactor);
|
276 |
|
277 |
|
278 | drawGridGap(
|
279 | context, highlight.rowGaps, highlight.gridHighlightConfig.rowGapColor,
|
280 | highlight.gridHighlightConfig.rowHatchColor, highlight.rotationAngle, emulationScaleFactor,
|
281 | true);
|
282 | drawGridGap(
|
283 | context, highlight.columnGaps, highlight.gridHighlightConfig.columnGapColor,
|
284 | highlight.gridHighlightConfig.columnHatchColor, highlight.rotationAngle, emulationScaleFactor,
|
285 | false);
|
286 |
|
287 |
|
288 | const areaBounds =
|
289 | drawGridAreas(context, highlight.areaNames, highlight.gridHighlightConfig.areaBorderColor, emulationScaleFactor);
|
290 |
|
291 |
|
292 | const writingModeMatrix = context.getTransform();
|
293 | writingModeMatrix.scaleSelf(1 / deviceScaleFactor);
|
294 | context.restore();
|
295 |
|
296 | if (highlight.gridHighlightConfig.showGridExtensionLines) {
|
297 | if (rowBounds) {
|
298 | drawExtendedGridLines(
|
299 | context, rowBounds, highlight.gridHighlightConfig.rowLineColor, highlight.gridHighlightConfig.rowLineDash,
|
300 | writingModeMatrix, canvasWidth, canvasHeight);
|
301 | }
|
302 | if (columnBounds) {
|
303 | drawExtendedGridLines(
|
304 | context, columnBounds, highlight.gridHighlightConfig.columnLineColor,
|
305 | highlight.gridHighlightConfig.columnLineDash, writingModeMatrix, canvasWidth, canvasHeight);
|
306 | }
|
307 | }
|
308 |
|
309 |
|
310 | drawGridLabels(
|
311 | highlight, gridBounds, areaBounds, {canvasWidth, canvasHeight}, labelState, emulationScaleFactor,
|
312 | writingModeMatrix);
|
313 | }
|
314 |
|
315 | function applyWritingModeTransformation(writingMode: string, gridBounds: Bounds, context: CanvasRenderingContext2D) {
|
316 | if (writingMode !== 'vertical-rl' && writingMode !== 'vertical-lr') {
|
317 | return;
|
318 | }
|
319 |
|
320 | const topLeft = gridBounds.allPoints[0];
|
321 | const bottomLeft = gridBounds.allPoints[3];
|
322 |
|
323 |
|
324 | context.translate(topLeft.x, topLeft.y);
|
325 |
|
326 | if (writingMode === 'vertical-rl') {
|
327 | context.rotate(90 * Math.PI / 180);
|
328 | context.translate(0, -1 * (bottomLeft.y - topLeft.y));
|
329 | }
|
330 |
|
331 | if (writingMode === 'vertical-lr') {
|
332 | context.rotate(90 * Math.PI / 180);
|
333 | context.scale(1, -1);
|
334 | }
|
335 |
|
336 |
|
337 | context.translate(topLeft.x * -1, topLeft.y * -1);
|
338 | }
|
339 |
|
340 | function drawGridLines(
|
341 | context: CanvasRenderingContext2D, highlight: GridHighlight, direction: 'row'|'column',
|
342 | emulationScaleFactor: number) {
|
343 | const tracks = highlight[`${direction}s` as 'rows' | 'columns'];
|
344 | const color = highlight.gridHighlightConfig[`${direction}LineColor` as 'rowLineColor' | 'columnLineColor'];
|
345 | const dash = highlight.gridHighlightConfig[`${direction}LineDash` as 'rowLineDash' | 'columnLineDash'];
|
346 |
|
347 | if (!color) {
|
348 | return null;
|
349 | }
|
350 |
|
351 | const bounds = emptyBounds();
|
352 | const path = buildPath(tracks, bounds, emulationScaleFactor);
|
353 |
|
354 | context.save();
|
355 | context.translate(0.5, 0.5);
|
356 | if (dash) {
|
357 | context.setLineDash([3, 3]);
|
358 | }
|
359 | context.lineWidth = 0;
|
360 | context.strokeStyle = color;
|
361 |
|
362 | context.save();
|
363 | context.stroke(path);
|
364 | context.restore();
|
365 |
|
366 | context.restore();
|
367 |
|
368 | return bounds;
|
369 | }
|
370 |
|
371 | function drawExtendedGridLines(
|
372 | context: CanvasRenderingContext2D, bounds: Bounds, color: string, dash: boolean|undefined,
|
373 | writingModeMatrix: DOMMatrix, canvasWidth: number, canvasHeight: number) {
|
374 | context.save();
|
375 | context.strokeStyle = color;
|
376 | context.lineWidth = 1;
|
377 | context.translate(0.5, 0.5);
|
378 | if (dash) {
|
379 | context.setLineDash([3, 3]);
|
380 | }
|
381 |
|
382 |
|
383 |
|
384 | for (let i = 0; i < bounds.allPoints.length; i += 2) {
|
385 | let point1 = applyMatrixToPoint(bounds.allPoints[i], writingModeMatrix);
|
386 | let point2 = applyMatrixToPoint(bounds.allPoints[i + 1], writingModeMatrix);
|
387 | let edgePoint1;
|
388 | let edgePoint2;
|
389 |
|
390 | if (point1.x === point2.x) {
|
391 |
|
392 | edgePoint1 = {x: point1.x, y: 0};
|
393 | edgePoint2 = {x: point1.x, y: canvasHeight};
|
394 | if (point2.y < point1.y) {
|
395 | [point1, point2] = [point2, point1];
|
396 | }
|
397 | } else if (point1.y === point2.y) {
|
398 |
|
399 | edgePoint1 = {x: 0, y: point1.y};
|
400 | edgePoint2 = {x: canvasWidth, y: point1.y};
|
401 | if (point2.x < point1.x) {
|
402 | [point1, point2] = [point2, point1];
|
403 | }
|
404 | } else {
|
405 |
|
406 | const a = (point2.y - point1.y) / (point2.x - point1.x);
|
407 | const b = (point1.y * point2.x - point2.y * point1.x) / (point2.x - point1.x);
|
408 |
|
409 | edgePoint1 = {x: 0, y: b};
|
410 | edgePoint2 = {x: canvasWidth, y: (canvasWidth * a) + b};
|
411 |
|
412 | if (point2.x < point1.x) {
|
413 | [point1, point2] = [point2, point1];
|
414 | }
|
415 | }
|
416 |
|
417 | context.beginPath();
|
418 | context.moveTo(edgePoint1.x, edgePoint1.y);
|
419 | context.lineTo(point1.x, point1.y);
|
420 | context.moveTo(point2.x, point2.y);
|
421 | context.lineTo(edgePoint2.x, edgePoint2.y);
|
422 | context.stroke();
|
423 | }
|
424 |
|
425 | context.restore();
|
426 | }
|
427 |
|
428 |
|
429 |
|
430 |
|
431 |
|
432 | function drawGridAreas(
|
433 | context: CanvasRenderingContext2D, areas: {[key: string]: Array<string|number>}, borderColor: string|undefined,
|
434 | emulationScaleFactor: number): AreaBounds[] {
|
435 | if (!areas || !Object.keys(areas).length) {
|
436 | return [];
|
437 | }
|
438 |
|
439 | context.save();
|
440 | if (borderColor) {
|
441 | context.strokeStyle = borderColor;
|
442 | }
|
443 | context.lineWidth = 2;
|
444 |
|
445 | const areaBounds = [];
|
446 |
|
447 | for (const name in areas) {
|
448 | const areaCommands = areas[name];
|
449 |
|
450 | const bounds = emptyBounds();
|
451 | const path = buildPath(areaCommands, bounds, emulationScaleFactor);
|
452 |
|
453 | context.stroke(path);
|
454 |
|
455 | areaBounds.push({name, bounds});
|
456 | }
|
457 |
|
458 | context.restore();
|
459 |
|
460 | return areaBounds;
|
461 | }
|
462 |
|
463 | function drawGridGap(
|
464 | context: CanvasRenderingContext2D, gapCommands: Array<number|string>, gapColor: string|undefined,
|
465 | hatchColor: string|undefined, rotationAngle: number, emulationScaleFactor: number,
|
466 | flipDirection: boolean|undefined) {
|
467 | if (!gapColor && !hatchColor) {
|
468 | return;
|
469 | }
|
470 |
|
471 | context.save();
|
472 | context.translate(0.5, 0.5);
|
473 | context.lineWidth = 0;
|
474 |
|
475 | const bounds = emptyBounds();
|
476 | const path = buildPath(gapCommands, bounds, emulationScaleFactor);
|
477 |
|
478 |
|
479 | if (gapColor) {
|
480 | context.fillStyle = gapColor;
|
481 | context.fill(path);
|
482 | }
|
483 |
|
484 |
|
485 | if (hatchColor) {
|
486 | hatchFillPath(context, path, bounds, 10, hatchColor, rotationAngle, flipDirection);
|
487 | }
|
488 | context.restore();
|
489 | }
|