1 |
|
2 |
|
3 |
|
4 |
|
5 | import {luminance} from '../front_end/core/common/ColorUtils.js';
|
6 |
|
7 | import type {AreaBounds, Bounds, Position} from './common.js';
|
8 | import {createChild} from './common.js';
|
9 | import {applyMatrixToPoint, parseHexa} from './highlight_common.js';
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 | const GridArrowTypes = {
|
35 | leftTop: 'left-top',
|
36 | leftMid: 'left-mid',
|
37 | leftBottom: 'left-bottom',
|
38 | topLeft: 'top-left',
|
39 | topMid: 'top-mid',
|
40 | topRight: 'top-right',
|
41 | rightTop: 'right-top',
|
42 | rightMid: 'right-mid',
|
43 | rightBottom: 'right-bottom',
|
44 | bottomLeft: 'bottom-left',
|
45 | bottomMid: 'bottom-mid',
|
46 | bottomRight: 'bottom-right',
|
47 | };
|
48 |
|
49 |
|
50 | const gridArrowWidth = 3;
|
51 |
|
52 |
|
53 | const gridPageMargin = 20;
|
54 |
|
55 |
|
56 | const gridLabelDistance = 20;
|
57 |
|
58 | const maxLineNamesCount = 3;
|
59 | const defaultLabelColor = '#1A73E8';
|
60 | const defaultLabelTextColor = '#121212';
|
61 |
|
62 | export interface CanvasSize {
|
63 | canvasWidth: number;
|
64 | canvasHeight: number;
|
65 | }
|
66 |
|
67 | interface PositionData {
|
68 | positions: Position[];
|
69 | hasFirst: boolean;
|
70 | hasLast: boolean;
|
71 | names?: string[][];
|
72 | }
|
73 |
|
74 | type PositionDataWithNames = PositionData&{
|
75 | names: string[][],
|
76 | };
|
77 |
|
78 | interface TracksPositionData {
|
79 | positive: PositionData;
|
80 | negative: PositionData;
|
81 | }
|
82 |
|
83 | interface TracksPositionDataWithNames {
|
84 | positive: PositionDataWithNames;
|
85 | negative: PositionDataWithNames;
|
86 | }
|
87 |
|
88 | interface GridPositionNormalizedData {
|
89 | rows: TracksPositionData;
|
90 | columns: TracksPositionData;
|
91 | bounds: Bounds;
|
92 | }
|
93 |
|
94 | export interface GridPositionNormalizedDataWithNames {
|
95 | rows: TracksPositionDataWithNames;
|
96 | columns: TracksPositionDataWithNames;
|
97 | bounds: Bounds;
|
98 | }
|
99 |
|
100 | interface TrackSize {
|
101 | computedSize: number;
|
102 | authoredSize?: number;
|
103 | x: number;
|
104 | y: number;
|
105 | }
|
106 |
|
107 | export interface GridHighlightOptions {
|
108 | gridBorderDash: boolean;
|
109 | rowLineDash: boolean;
|
110 | columnLineDash: boolean;
|
111 | showGridExtensionLines: boolean;
|
112 | showPositiveLineNumbers: boolean;
|
113 | showNegativeLineNumbers: boolean;
|
114 | rowLineColor?: string;
|
115 | columnLineColor?: string;
|
116 | rowHatchColor: string;
|
117 | columnHatchColor: string;
|
118 | showLineNames: boolean;
|
119 | }
|
120 |
|
121 | export interface GridHighlightConfig {
|
122 | rotationAngle?: number;
|
123 | writingMode?: string;
|
124 | columnTrackSizes?: TrackSize[];
|
125 | rowTrackSizes?: TrackSize[];
|
126 | positiveRowLineNumberPositions?: Position[];
|
127 | negativeRowLineNumberPositions?: Position[];
|
128 | positiveColumnLineNumberPositions?: Position[];
|
129 | negativeColumnLineNumberPositions?: Position[];
|
130 | rowLineNameOffsets?: {name: string, x: number, y: number}[];
|
131 | columnLineNameOffsets?: {name: string, x: number, y: number}[];
|
132 | gridHighlightConfig?: GridHighlightOptions;
|
133 | }
|
134 |
|
135 | interface LabelSize {
|
136 | width: number;
|
137 | height: number;
|
138 | mainSize: number;
|
139 | crossSize: number;
|
140 | }
|
141 |
|
142 | export interface GridLabelState {
|
143 | gridLayerCounter: number;
|
144 | }
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 | export function drawGridLabels(
|
151 | config: GridHighlightConfig, gridBounds: Bounds, areaBounds: AreaBounds[], canvasSize: CanvasSize,
|
152 | labelState: GridLabelState, emulationScaleFactor: number,
|
153 | writingModeMatrix: DOMMatrix|undefined = new DOMMatrix()) {
|
154 |
|
155 |
|
156 |
|
157 | const labelContainerId = `grid-${labelState.gridLayerCounter++}-labels`;
|
158 | let labelContainerForNode = document.getElementById(labelContainerId);
|
159 | if (!labelContainerForNode) {
|
160 | const mainLabelLayerContainer = document.getElementById('grid-label-container');
|
161 | if (!mainLabelLayerContainer) {
|
162 | throw new Error('#grid-label-container is not found');
|
163 | }
|
164 | labelContainerForNode = createChild(mainLabelLayerContainer, 'div');
|
165 | labelContainerForNode.id = labelContainerId;
|
166 | }
|
167 |
|
168 | const rowColor = config.gridHighlightConfig && config.gridHighlightConfig.rowLineColor ?
|
169 | config.gridHighlightConfig.rowLineColor :
|
170 | defaultLabelColor;
|
171 | const rowTextColor = generateLegibleTextColor(rowColor);
|
172 |
|
173 | labelContainerForNode.style.setProperty('--row-label-color', rowColor);
|
174 | labelContainerForNode.style.setProperty('--row-label-text-color', rowTextColor);
|
175 |
|
176 | const columnColor = config.gridHighlightConfig && config.gridHighlightConfig.columnLineColor ?
|
177 | config.gridHighlightConfig.columnLineColor :
|
178 | defaultLabelColor;
|
179 | const columnTextColor = generateLegibleTextColor(columnColor);
|
180 |
|
181 | labelContainerForNode.style.setProperty('--column-label-color', columnColor);
|
182 | labelContainerForNode.style.setProperty('--column-label-text-color', columnTextColor);
|
183 |
|
184 | labelContainerForNode.innerText = '';
|
185 |
|
186 |
|
187 | const areaNameContainer = createChild(labelContainerForNode, 'div', 'area-names');
|
188 | const lineNameContainer = createChild(labelContainerForNode, 'div', 'line-names');
|
189 | const lineNumberContainer = createChild(labelContainerForNode, 'div', 'line-numbers');
|
190 | const trackSizesContainer = createChild(labelContainerForNode, 'div', 'track-sizes');
|
191 |
|
192 |
|
193 | const normalizedData = normalizePositionData(config, gridBounds);
|
194 | if (config.gridHighlightConfig && config.gridHighlightConfig.showLineNames) {
|
195 | drawGridLineNames(
|
196 | lineNameContainer, normalizedData as GridPositionNormalizedDataWithNames, canvasSize, emulationScaleFactor,
|
197 | writingModeMatrix, config.writingMode);
|
198 | } else {
|
199 | drawGridLineNumbers(
|
200 | lineNumberContainer, normalizedData, canvasSize, emulationScaleFactor, writingModeMatrix, config.writingMode);
|
201 | }
|
202 |
|
203 |
|
204 | drawGridAreaNames(areaNameContainer, areaBounds, writingModeMatrix, config.writingMode);
|
205 |
|
206 | if (config.columnTrackSizes) {
|
207 |
|
208 | drawGridTrackSizes(
|
209 | trackSizesContainer, config.columnTrackSizes, 'column', canvasSize, emulationScaleFactor, writingModeMatrix,
|
210 | config.writingMode);
|
211 | }
|
212 | if (config.rowTrackSizes) {
|
213 |
|
214 | drawGridTrackSizes(
|
215 | trackSizesContainer, config.rowTrackSizes, 'row', canvasSize, emulationScaleFactor, writingModeMatrix,
|
216 | config.writingMode);
|
217 | }
|
218 | }
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 | function* positionIterator(positions: Position[], axis: 'x'|'y'): Generator<[number, Position]> {
|
225 | let lastEmittedPos = null;
|
226 |
|
227 | for (const [i, pos] of positions.entries()) {
|
228 |
|
229 | const isFirst = i === 0;
|
230 |
|
231 | const isLast = i === positions.length - 1;
|
232 |
|
233 | const isFarEnoughFromPrevious =
|
234 | Math.abs(pos[axis] - (lastEmittedPos ? lastEmittedPos[axis] : 0)) > gridLabelDistance;
|
235 |
|
236 | const isFarEnoughFromLast =
|
237 | !isLast && Math.abs(positions[positions.length - 1][axis] - pos[axis]) > gridLabelDistance;
|
238 |
|
239 | if (isFirst || isLast || (isFarEnoughFromPrevious && isFarEnoughFromLast)) {
|
240 | yield [i, pos];
|
241 | lastEmittedPos = pos;
|
242 | }
|
243 | }
|
244 | }
|
245 |
|
246 | const last = <T>(array: T[]) => array[array.length - 1];
|
247 | const first = <T>(array: T[]) => array[0];
|
248 |
|
249 |
|
250 |
|
251 |
|
252 | function normalizeNameData(namePositions: {name: string, x: number, y: number}[]):
|
253 | {positions: {x: number, y: number}[], names: string[][]} {
|
254 | const positions = [];
|
255 | const names = [];
|
256 |
|
257 | for (const {name, x, y} of namePositions) {
|
258 | const normalizedX = Math.round(x);
|
259 | const normalizedY = Math.round(y);
|
260 |
|
261 |
|
262 |
|
263 | const existingIndex = positions.findIndex(({x, y}) => x === normalizedX && y === normalizedY);
|
264 | if (existingIndex > -1) {
|
265 | names[existingIndex].push(name);
|
266 | } else {
|
267 | positions.push({x: normalizedX, y: normalizedY});
|
268 | names.push([name]);
|
269 | }
|
270 | }
|
271 |
|
272 | return {positions, names};
|
273 | }
|
274 |
|
275 | export interface NormalizePositionDataConfig {
|
276 | positiveRowLineNumberPositions?: Position[];
|
277 | negativeRowLineNumberPositions?: Position[];
|
278 | positiveColumnLineNumberPositions?: Position[];
|
279 | negativeColumnLineNumberPositions?: Position[];
|
280 | rowLineNameOffsets?: {name: string, x: number, y: number}[];
|
281 | columnLineNameOffsets?: {name: string, x: number, y: number}[];
|
282 | gridHighlightConfig?: {showLineNames: boolean};
|
283 | }
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 | export function normalizePositionData(config: NormalizePositionDataConfig, bounds: Bounds): GridPositionNormalizedData {
|
295 | const width = Math.round(bounds.maxX - bounds.minX);
|
296 | const height = Math.round(bounds.maxY - bounds.minY);
|
297 |
|
298 | const data = {
|
299 | rows: {
|
300 | positive: {positions: [] as Position[], hasFirst: false, hasLast: false},
|
301 | negative: {positions: [] as Position[], hasFirst: false, hasLast: false},
|
302 | },
|
303 | columns: {
|
304 | positive: {positions: [] as Position[], hasFirst: false, hasLast: false},
|
305 | negative: {positions: [] as Position[], hasFirst: false, hasLast: false},
|
306 | },
|
307 | bounds: {
|
308 | minX: Math.round(bounds.minX),
|
309 | maxX: Math.round(bounds.maxX),
|
310 | minY: Math.round(bounds.minY),
|
311 | maxY: Math.round(bounds.maxY),
|
312 | allPoints: bounds.allPoints,
|
313 | width,
|
314 | height,
|
315 | },
|
316 | };
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 | if (config.gridHighlightConfig && config.gridHighlightConfig.showLineNames) {
|
323 | const rowData = normalizeNameData(config.rowLineNameOffsets || []);
|
324 | const positiveRows: PositionDataWithNames = {
|
325 | positions: rowData.positions,
|
326 | names: rowData.names,
|
327 | hasFirst: rowData.positions.length ? first(rowData.positions).y === data.bounds.minY : false,
|
328 | hasLast: rowData.positions.length ? last(rowData.positions).y === data.bounds.maxY : false,
|
329 | };
|
330 | data.rows.positive = positiveRows;
|
331 |
|
332 | const columnData = normalizeNameData(config.columnLineNameOffsets || []);
|
333 | const positiveColumns: PositionDataWithNames = {
|
334 | positions: columnData.positions,
|
335 | names: columnData.names,
|
336 | hasFirst: columnData.positions.length ? first(columnData.positions).x === data.bounds.minX : false,
|
337 | hasLast: columnData.positions.length ? last(columnData.positions).x === data.bounds.maxX : false,
|
338 | };
|
339 | data.columns.positive = positiveColumns;
|
340 | } else {
|
341 | const normalizeXY = ({x, y}: {x: number, y: number}) => ({x: Math.round(x), y: Math.round(y)});
|
342 |
|
343 | if (config.positiveRowLineNumberPositions) {
|
344 | data.rows.positive = {
|
345 | positions: config.positiveRowLineNumberPositions.map(normalizeXY),
|
346 | hasFirst: Math.round(first(config.positiveRowLineNumberPositions).y) === data.bounds.minY,
|
347 | hasLast: Math.round(last(config.positiveRowLineNumberPositions).y) === data.bounds.maxY,
|
348 | };
|
349 | }
|
350 |
|
351 | if (config.negativeRowLineNumberPositions) {
|
352 | data.rows.negative = {
|
353 | positions: config.negativeRowLineNumberPositions.map(normalizeXY),
|
354 | hasFirst: Math.round(first(config.negativeRowLineNumberPositions).y) === data.bounds.minY,
|
355 | hasLast: Math.round(last(config.negativeRowLineNumberPositions).y) === data.bounds.maxY,
|
356 | };
|
357 | }
|
358 |
|
359 | if (config.positiveColumnLineNumberPositions) {
|
360 | data.columns.positive = {
|
361 | positions: config.positiveColumnLineNumberPositions.map(normalizeXY),
|
362 | hasFirst: Math.round(first(config.positiveColumnLineNumberPositions).x) === data.bounds.minX,
|
363 | hasLast: Math.round(last(config.positiveColumnLineNumberPositions).x) === data.bounds.maxX,
|
364 | };
|
365 | }
|
366 |
|
367 | if (config.negativeColumnLineNumberPositions) {
|
368 | data.columns.negative = {
|
369 | positions: config.negativeColumnLineNumberPositions.map(normalizeXY),
|
370 | hasFirst: Math.round(first(config.negativeColumnLineNumberPositions).x) === data.bounds.minX,
|
371 | hasLast: Math.round(last(config.negativeColumnLineNumberPositions).x) === data.bounds.maxX,
|
372 | };
|
373 | }
|
374 | }
|
375 |
|
376 | return data;
|
377 | }
|
378 |
|
379 |
|
380 |
|
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 |
|
387 | export function drawGridLineNumbers(
|
388 | container: HTMLElement, data: GridPositionNormalizedData, canvasSize: CanvasSize, emulationScaleFactor: number,
|
389 | writingModeMatrix: DOMMatrix|undefined = new DOMMatrix(), writingMode: string|undefined = 'horizontal-tb') {
|
390 | if (!data.columns.positive.names) {
|
391 | for (const [i, pos] of positionIterator(data.columns.positive.positions, 'x')) {
|
392 | const element = createLabelElement(container, (i + 1).toString(), 'column');
|
393 | placePositiveColumnLabel(
|
394 | element, applyMatrixToPoint(pos, writingModeMatrix), data, writingMode, canvasSize, emulationScaleFactor);
|
395 | }
|
396 | }
|
397 |
|
398 | if (!data.rows.positive.names) {
|
399 | for (const [i, pos] of positionIterator(data.rows.positive.positions, 'y')) {
|
400 | const element = createLabelElement(container, (i + 1).toString(), 'row');
|
401 | placePositiveRowLabel(
|
402 | element, applyMatrixToPoint(pos, writingModeMatrix), data, writingMode, canvasSize, emulationScaleFactor);
|
403 | }
|
404 | }
|
405 |
|
406 | for (const [i, pos] of positionIterator(data.columns.negative.positions, 'x')) {
|
407 |
|
408 | const element =
|
409 | createLabelElement(container, (data.columns.negative.positions.length * -1 + i).toString(), 'column');
|
410 | placeNegativeColumnLabel(
|
411 | element, applyMatrixToPoint(pos, writingModeMatrix), data, writingMode, canvasSize, emulationScaleFactor);
|
412 | }
|
413 |
|
414 | for (const [i, pos] of positionIterator(data.rows.negative.positions, 'y')) {
|
415 |
|
416 | const element = createLabelElement(container, (data.rows.negative.positions.length * -1 + i).toString(), 'row');
|
417 | placeNegativeRowLabel(
|
418 | element, applyMatrixToPoint(pos, writingModeMatrix), data, writingMode, canvasSize, emulationScaleFactor);
|
419 | }
|
420 | }
|
421 |
|
422 |
|
423 |
|
424 |
|
425 | export function drawGridTrackSizes(
|
426 | container: HTMLElement, trackSizes: Array<TrackSize>, direction: 'row'|'column', canvasSize: CanvasSize,
|
427 | emulationScaleFactor: number, writingModeMatrix: DOMMatrix|undefined = new DOMMatrix(),
|
428 | writingMode: string|undefined = 'horizontal-tb') {
|
429 | const {main, cross} = getAxes(writingMode);
|
430 | const {crossSize} = getCanvasSizes(writingMode, canvasSize);
|
431 |
|
432 | for (const {x, y, computedSize, authoredSize} of trackSizes) {
|
433 | const point = applyMatrixToPoint({x, y}, writingModeMatrix);
|
434 |
|
435 | const size = computedSize.toFixed(2);
|
436 | const formattedComputed = `${size.endsWith('.00') ? size.slice(0, -3) : size}px`;
|
437 | const element =
|
438 | createLabelElement(container, `${authoredSize ? authoredSize + '·' : ''}${formattedComputed}`, direction);
|
439 | const labelSize = getLabelSize(element, writingMode);
|
440 |
|
441 | let flipIn = point[main] - labelSize.mainSize < gridPageMargin;
|
442 | if (direction === 'column') {
|
443 | flipIn = writingMode === 'vertical-rl' ? crossSize - point[cross] - labelSize.crossSize < gridPageMargin :
|
444 | point[cross] - labelSize.crossSize < gridPageMargin;
|
445 | }
|
446 |
|
447 | let arrowType = adaptArrowTypeForWritingMode(
|
448 | direction === 'column' ? GridArrowTypes.bottomMid : GridArrowTypes.rightMid, writingMode);
|
449 | arrowType = flipArrowTypeIfNeeded(arrowType, flipIn);
|
450 |
|
451 | placeLineLabel(element, arrowType, point.x, point.y, labelSize, emulationScaleFactor);
|
452 | }
|
453 | }
|
454 |
|
455 |
|
456 |
|
457 |
|
458 | export function drawGridLineNames(
|
459 | container: HTMLElement, data: GridPositionNormalizedDataWithNames, canvasSize: CanvasSize,
|
460 | emulationScaleFactor: number, writingModeMatrix: DOMMatrix|undefined = new DOMMatrix(),
|
461 | writingMode: string|undefined = 'horizontal-tb') {
|
462 | for (const [i, pos] of data.columns.positive.positions.entries()) {
|
463 | const names = data.columns.positive.names[i];
|
464 | const element = createLabelElement(container, makeLineNameLabelContent(names), 'column');
|
465 | placePositiveColumnLabel(
|
466 | element, applyMatrixToPoint(pos, writingModeMatrix), data, writingMode, canvasSize, emulationScaleFactor);
|
467 | }
|
468 |
|
469 | for (const [i, pos] of data.rows.positive.positions.entries()) {
|
470 | const names = data.rows.positive.names[i];
|
471 | const element = createLabelElement(container, makeLineNameLabelContent(names), 'row');
|
472 | placePositiveRowLabel(
|
473 | element, applyMatrixToPoint(pos, writingModeMatrix), data, writingMode, canvasSize, emulationScaleFactor);
|
474 | }
|
475 | }
|
476 |
|
477 |
|
478 |
|
479 |
|
480 | function makeLineNameLabelContent(names: string[]): HTMLElement {
|
481 | const content = document.createElement('ul');
|
482 | const namesToDisplay = names.slice(0, maxLineNamesCount);
|
483 |
|
484 | for (const name of namesToDisplay) {
|
485 | createChild(content, 'li', 'line-name').textContent = name;
|
486 | }
|
487 |
|
488 | return content;
|
489 | }
|
490 |
|
491 |
|
492 |
|
493 |
|
494 | export function drawGridAreaNames(
|
495 | container: HTMLElement, areaBounds: AreaBounds[], writingModeMatrix: DOMMatrix|undefined = new DOMMatrix(),
|
496 | writingMode: string|undefined = 'horizontal-tb') {
|
497 | for (const {name, bounds} of areaBounds) {
|
498 | const element = createLabelElement(container, name, 'row');
|
499 | const {width, height} = getLabelSize(element, writingMode);
|
500 |
|
501 |
|
502 |
|
503 | const point = writingMode === 'vertical-rl' ? bounds.allPoints[3] : bounds.allPoints[0];
|
504 | const corner = applyMatrixToPoint(point, writingModeMatrix);
|
505 |
|
506 | const flipX = bounds.allPoints[1].x < bounds.allPoints[0].x;
|
507 | const flipY = bounds.allPoints[3].y < bounds.allPoints[0].y;
|
508 | element.style.left = (corner.x - (flipX ? width : 0)) + 'px';
|
509 | element.style.top = (corner.y - (flipY ? height : 0)) + 'px';
|
510 | }
|
511 | }
|
512 |
|
513 |
|
514 |
|
515 |
|
516 | function createLabelElement(
|
517 | container: HTMLElement, textContent: string|HTMLElement, direction: 'row'|'column'): HTMLElement {
|
518 | const wrapper = createChild(container, 'div');
|
519 | const element = createChild(wrapper, 'div', 'grid-label-content');
|
520 | element.dataset.direction = direction;
|
521 |
|
522 | if (typeof textContent === 'string') {
|
523 | element.textContent = textContent;
|
524 | } else {
|
525 | element.appendChild(textContent);
|
526 | }
|
527 |
|
528 | return element;
|
529 | }
|
530 |
|
531 |
|
532 |
|
533 |
|
534 | function getLabelSideEdgePoints(
|
535 | gridBounds: Bounds, direction: string, side: string): {start: {x: number, y: number}, end: {x: number, y: number}} {
|
536 | const [p1, p2, p3, p4] = gridBounds.allPoints;
|
537 |
|
538 |
|
539 |
|
540 |
|
541 |
|
542 |
|
543 |
|
544 |
|
545 | if (direction === 'row') {
|
546 | return side === 'positive' ? {start: p1, end: p4} : {start: p2, end: p3};
|
547 | }
|
548 |
|
549 | return side === 'positive' ? {start: p1, end: p2} : {start: p4, end: p3};
|
550 | }
|
551 |
|
552 |
|
553 |
|
554 |
|
555 |
|
556 |
|
557 |
|
558 | function getAxes(writingMode: string): {main: 'x'|'y', cross: 'x'|'y'} {
|
559 | return writingMode.startsWith('vertical') ? {main: 'y', cross: 'x'} : {main: 'x', cross: 'y'};
|
560 | }
|
561 |
|
562 |
|
563 |
|
564 |
|
565 |
|
566 |
|
567 |
|
568 | function getCanvasSizes(writingMode: string, canvasSize: CanvasSize): {mainSize: number, crossSize: number} {
|
569 | return writingMode.startsWith('vertical') ? {mainSize: canvasSize.canvasHeight, crossSize: canvasSize.canvasWidth} :
|
570 | {mainSize: canvasSize.canvasWidth, crossSize: canvasSize.canvasHeight};
|
571 | }
|
572 |
|
573 |
|
574 |
|
575 |
|
576 | function placePositiveRowLabel(
|
577 | element: HTMLElement, pos: Position, data: GridPositionNormalizedData, writingMode: string, canvasSize: CanvasSize,
|
578 | emulationScaleFactor: number) {
|
579 | const {start, end} = getLabelSideEdgePoints(data.bounds, 'row', 'positive');
|
580 | const {main, cross} = getAxes(writingMode);
|
581 | const {crossSize} = getCanvasSizes(writingMode, canvasSize);
|
582 | const labelSize = getLabelSize(element, writingMode);
|
583 |
|
584 | const isAtSharedStartCorner = pos[cross] === start[cross] && data.columns && data.columns.positive.hasFirst;
|
585 | const isAtSharedEndCorner = pos[cross] === end[cross] && data.columns && data.columns.negative.hasFirst;
|
586 | const isTooCloseToViewportStart = pos[cross] < gridPageMargin;
|
587 | const isTooCloseToViewportEnd = crossSize - pos[cross] < gridPageMargin;
|
588 | const flipIn = pos[main] - labelSize.mainSize < gridPageMargin;
|
589 |
|
590 | if (flipIn && (isAtSharedStartCorner || isAtSharedEndCorner)) {
|
591 | element.classList.add('inner-shared-corner');
|
592 | }
|
593 |
|
594 | let arrowType = adaptArrowTypeForWritingMode(GridArrowTypes.rightMid, writingMode);
|
595 | if (isTooCloseToViewportStart || isAtSharedStartCorner) {
|
596 | arrowType = adaptArrowTypeForWritingMode(GridArrowTypes.rightTop, writingMode);
|
597 | } else if (isTooCloseToViewportEnd || isAtSharedEndCorner) {
|
598 | arrowType = adaptArrowTypeForWritingMode(GridArrowTypes.rightBottom, writingMode);
|
599 | }
|
600 | arrowType = flipArrowTypeIfNeeded(arrowType, flipIn);
|
601 |
|
602 | placeLineLabel(element, arrowType, pos.x, pos.y, labelSize, emulationScaleFactor);
|
603 | }
|
604 |
|
605 |
|
606 |
|
607 |
|
608 | function placeNegativeRowLabel(
|
609 | element: HTMLElement, pos: Position, data: GridPositionNormalizedData, writingMode: string, canvasSize: CanvasSize,
|
610 | emulationScaleFactor: number) {
|
611 | const {start, end} = getLabelSideEdgePoints(data.bounds, 'row', 'negative');
|
612 | const {main, cross} = getAxes(writingMode);
|
613 | const {mainSize, crossSize} = getCanvasSizes(writingMode, canvasSize);
|
614 | const labelSize = getLabelSize(element, writingMode);
|
615 |
|
616 | const isAtSharedStartCorner = pos[cross] === start[cross] && data.columns && data.columns.positive.hasLast;
|
617 | const isAtSharedEndCorner = pos[cross] === end[cross] && data.columns && data.columns.negative.hasLast;
|
618 | const isTooCloseToViewportStart = pos[cross] < gridPageMargin;
|
619 | const isTooCloseToViewportEnd = crossSize - pos[cross] < gridPageMargin;
|
620 | const flipIn = mainSize - pos[main] - labelSize.mainSize < gridPageMargin;
|
621 |
|
622 | if (flipIn && (isAtSharedStartCorner || isAtSharedEndCorner)) {
|
623 | element.classList.add('inner-shared-corner');
|
624 | }
|
625 |
|
626 | let arrowType = adaptArrowTypeForWritingMode(GridArrowTypes.leftMid, writingMode);
|
627 | if (isTooCloseToViewportStart || isAtSharedStartCorner) {
|
628 | arrowType = adaptArrowTypeForWritingMode(GridArrowTypes.leftTop, writingMode);
|
629 | } else if (isTooCloseToViewportEnd || isAtSharedEndCorner) {
|
630 | arrowType = adaptArrowTypeForWritingMode(GridArrowTypes.leftBottom, writingMode);
|
631 | }
|
632 | arrowType = flipArrowTypeIfNeeded(arrowType, flipIn);
|
633 |
|
634 | placeLineLabel(element, arrowType, pos.x, pos.y, labelSize, emulationScaleFactor);
|
635 | }
|
636 |
|
637 |
|
638 |
|
639 |
|
640 | function placePositiveColumnLabel(
|
641 | element: HTMLElement, pos: Position, data: GridPositionNormalizedData, writingMode: string, canvasSize: CanvasSize,
|
642 | emulationScaleFactor: number) {
|
643 | const {start, end} = getLabelSideEdgePoints(data.bounds, 'column', 'positive');
|
644 | const {main, cross} = getAxes(writingMode);
|
645 | const {mainSize, crossSize} = getCanvasSizes(writingMode, canvasSize);
|
646 | const labelSize = getLabelSize(element, writingMode);
|
647 |
|
648 | const isAtSharedStartCorner = pos[main] === start[main] && data.rows && data.rows.positive.hasFirst;
|
649 | const isAtSharedEndCorner = pos[main] === end[main] && data.rows && data.rows.negative.hasFirst;
|
650 | const isTooCloseToViewportStart = pos[main] < gridPageMargin;
|
651 | const isTooCloseToViewportEnd = mainSize - pos[main] < gridPageMargin;
|
652 | const flipIn = writingMode === 'vertical-rl' ? crossSize - pos[cross] - labelSize.crossSize < gridPageMargin :
|
653 | pos[cross] - labelSize.crossSize < gridPageMargin;
|
654 |
|
655 | if (flipIn && (isAtSharedStartCorner || isAtSharedEndCorner)) {
|
656 | element.classList.add('inner-shared-corner');
|
657 | }
|
658 |
|
659 | let arrowType = adaptArrowTypeForWritingMode(GridArrowTypes.bottomMid, writingMode);
|
660 | if (isTooCloseToViewportStart) {
|
661 | arrowType = adaptArrowTypeForWritingMode(GridArrowTypes.bottomLeft, writingMode);
|
662 | } else if (isTooCloseToViewportEnd) {
|
663 | arrowType = adaptArrowTypeForWritingMode(GridArrowTypes.bottomRight, writingMode);
|
664 | }
|
665 |
|
666 | arrowType = flipArrowTypeIfNeeded(arrowType, flipIn);
|
667 |
|
668 | placeLineLabel(element, arrowType, pos.x, pos.y, labelSize, emulationScaleFactor);
|
669 | }
|
670 |
|
671 |
|
672 |
|
673 |
|
674 | function placeNegativeColumnLabel(
|
675 | element: HTMLElement, pos: Position, data: GridPositionNormalizedData, writingMode: string, canvasSize: CanvasSize,
|
676 | emulationScaleFactor: number) {
|
677 | const {start, end} = getLabelSideEdgePoints(data.bounds, 'column', 'negative');
|
678 | const {main, cross} = getAxes(writingMode);
|
679 | const {mainSize, crossSize} = getCanvasSizes(writingMode, canvasSize);
|
680 | const labelSize = getLabelSize(element, writingMode);
|
681 |
|
682 | const isAtSharedStartCorner = pos[main] === start[main] && data.rows && data.rows.positive.hasLast;
|
683 | const isAtSharedEndCorner = pos[main] === end[main] && data.rows && data.rows.negative.hasLast;
|
684 | const isTooCloseToViewportStart = pos[main] < gridPageMargin;
|
685 | const isTooCloseToViewportEnd = mainSize - pos[main] < gridPageMargin;
|
686 | const flipIn = writingMode === 'vertical-rl' ? pos[cross] - labelSize.crossSize < gridPageMargin :
|
687 | crossSize - pos[cross] - labelSize.crossSize < gridPageMargin;
|
688 |
|
689 | if (flipIn && (isAtSharedStartCorner || isAtSharedEndCorner)) {
|
690 | element.classList.add('inner-shared-corner');
|
691 | }
|
692 |
|
693 | let arrowType = adaptArrowTypeForWritingMode(GridArrowTypes.topMid, writingMode);
|
694 | if (isTooCloseToViewportStart) {
|
695 | arrowType = adaptArrowTypeForWritingMode(GridArrowTypes.topLeft, writingMode);
|
696 | } else if (isTooCloseToViewportEnd) {
|
697 | arrowType = adaptArrowTypeForWritingMode(GridArrowTypes.topRight, writingMode);
|
698 | }
|
699 | arrowType = flipArrowTypeIfNeeded(arrowType, flipIn);
|
700 |
|
701 | placeLineLabel(element, arrowType, pos.x, pos.y, labelSize, emulationScaleFactor);
|
702 | }
|
703 |
|
704 |
|
705 |
|
706 |
|
707 |
|
708 |
|
709 |
|
710 |
|
711 | function placeLineLabel(
|
712 | element: HTMLElement, arrowType: string, x: number, y: number, labelSize: LabelSize, emulationScaleFactor: number) {
|
713 | const {contentLeft, contentTop} =
|
714 | getLabelPositionByArrowType(arrowType, x, y, labelSize.width, labelSize.height, emulationScaleFactor);
|
715 |
|
716 | element.classList.add(arrowType);
|
717 | element.style.left = contentLeft + 'px';
|
718 | element.style.top = contentTop + 'px';
|
719 | }
|
720 |
|
721 |
|
722 |
|
723 |
|
724 |
|
725 | function getLabelSize(element: HTMLElement, writingMode: string): LabelSize {
|
726 | const width = getAdjustedLabelWidth(element);
|
727 | const height = element.getBoundingClientRect().height;
|
728 | const mainSize = writingMode.startsWith('vertical') ? height : width;
|
729 | const crossSize = writingMode.startsWith('vertical') ? width : height;
|
730 |
|
731 | return {width, height, mainSize, crossSize};
|
732 | }
|
733 |
|
734 |
|
735 |
|
736 |
|
737 |
|
738 | function getAdjustedLabelWidth(element: HTMLElement) {
|
739 | let labelWidth = element.getBoundingClientRect().width;
|
740 | if (labelWidth % 2 === 1) {
|
741 | labelWidth += 1;
|
742 | element.style.width = labelWidth + 'px';
|
743 | }
|
744 | return labelWidth;
|
745 | }
|
746 |
|
747 |
|
748 |
|
749 |
|
750 |
|
751 |
|
752 |
|
753 |
|
754 |
|
755 |
|
756 |
|
757 |
|
758 |
|
759 |
|
760 |
|
761 |
|
762 |
|
763 |
|
764 |
|
765 |
|
766 |
|
767 |
|
768 |
|
769 |
|
770 |
|
771 | function flipArrowTypeIfNeeded(arrowType: string, flipIn: boolean): string {
|
772 | if (!flipIn) {
|
773 | return arrowType;
|
774 | }
|
775 |
|
776 | switch (arrowType) {
|
777 | case GridArrowTypes.leftTop:
|
778 | return GridArrowTypes.rightTop;
|
779 | case GridArrowTypes.leftMid:
|
780 | return GridArrowTypes.rightMid;
|
781 | case GridArrowTypes.leftBottom:
|
782 | return GridArrowTypes.rightBottom;
|
783 | case GridArrowTypes.rightTop:
|
784 | return GridArrowTypes.leftTop;
|
785 | case GridArrowTypes.rightMid:
|
786 | return GridArrowTypes.leftMid;
|
787 | case GridArrowTypes.rightBottom:
|
788 | return GridArrowTypes.leftBottom;
|
789 | case GridArrowTypes.topLeft:
|
790 | return GridArrowTypes.bottomLeft;
|
791 | case GridArrowTypes.topMid:
|
792 | return GridArrowTypes.bottomMid;
|
793 | case GridArrowTypes.topRight:
|
794 | return GridArrowTypes.bottomRight;
|
795 | case GridArrowTypes.bottomLeft:
|
796 | return GridArrowTypes.topLeft;
|
797 | case GridArrowTypes.bottomMid:
|
798 | return GridArrowTypes.topMid;
|
799 | case GridArrowTypes.bottomRight:
|
800 | return GridArrowTypes.topRight;
|
801 | }
|
802 |
|
803 | return arrowType;
|
804 | }
|
805 |
|
806 |
|
807 |
|
808 |
|
809 |
|
810 | function adaptArrowTypeForWritingMode(arrowType: string, writingMode: string): string {
|
811 | if (writingMode === 'vertical-lr') {
|
812 | switch (arrowType) {
|
813 | case GridArrowTypes.leftTop:
|
814 | return GridArrowTypes.topLeft;
|
815 | case GridArrowTypes.leftMid:
|
816 | return GridArrowTypes.topMid;
|
817 | case GridArrowTypes.leftBottom:
|
818 | return GridArrowTypes.topRight;
|
819 | case GridArrowTypes.topLeft:
|
820 | return GridArrowTypes.leftTop;
|
821 | case GridArrowTypes.topMid:
|
822 | return GridArrowTypes.leftMid;
|
823 | case GridArrowTypes.topRight:
|
824 | return GridArrowTypes.leftBottom;
|
825 | case GridArrowTypes.rightTop:
|
826 | return GridArrowTypes.bottomRight;
|
827 | case GridArrowTypes.rightMid:
|
828 | return GridArrowTypes.bottomMid;
|
829 | case GridArrowTypes.rightBottom:
|
830 | return GridArrowTypes.bottomLeft;
|
831 | case GridArrowTypes.bottomLeft:
|
832 | return GridArrowTypes.rightTop;
|
833 | case GridArrowTypes.bottomMid:
|
834 | return GridArrowTypes.rightMid;
|
835 | case GridArrowTypes.bottomRight:
|
836 | return GridArrowTypes.rightBottom;
|
837 | }
|
838 | }
|
839 |
|
840 | if (writingMode === 'vertical-rl') {
|
841 | switch (arrowType) {
|
842 | case GridArrowTypes.leftTop:
|
843 | return GridArrowTypes.topRight;
|
844 | case GridArrowTypes.leftMid:
|
845 | return GridArrowTypes.topMid;
|
846 | case GridArrowTypes.leftBottom:
|
847 | return GridArrowTypes.topLeft;
|
848 | case GridArrowTypes.topLeft:
|
849 | return GridArrowTypes.rightTop;
|
850 | case GridArrowTypes.topMid:
|
851 | return GridArrowTypes.rightMid;
|
852 | case GridArrowTypes.topRight:
|
853 | return GridArrowTypes.rightBottom;
|
854 | case GridArrowTypes.rightTop:
|
855 | return GridArrowTypes.bottomRight;
|
856 | case GridArrowTypes.rightMid:
|
857 | return GridArrowTypes.bottomMid;
|
858 | case GridArrowTypes.rightBottom:
|
859 | return GridArrowTypes.bottomLeft;
|
860 | case GridArrowTypes.bottomLeft:
|
861 | return GridArrowTypes.leftTop;
|
862 | case GridArrowTypes.bottomMid:
|
863 | return GridArrowTypes.leftMid;
|
864 | case GridArrowTypes.bottomRight:
|
865 | return GridArrowTypes.leftBottom;
|
866 | }
|
867 | }
|
868 |
|
869 | return arrowType;
|
870 | }
|
871 |
|
872 |
|
873 |
|
874 |
|
875 |
|
876 | function getLabelPositionByArrowType(
|
877 | arrowType: string, x: number, y: number, labelWidth: number, labelHeight: number,
|
878 | emulationScaleFactor: number): {contentTop: number, contentLeft: number} {
|
879 | let contentTop = 0;
|
880 | let contentLeft = 0;
|
881 | x *= emulationScaleFactor;
|
882 | y *= emulationScaleFactor;
|
883 | switch (arrowType) {
|
884 | case GridArrowTypes.leftTop:
|
885 | contentTop = y;
|
886 | contentLeft = x + gridArrowWidth;
|
887 | break;
|
888 | case GridArrowTypes.leftMid:
|
889 | contentTop = y - (labelHeight / 2);
|
890 | contentLeft = x + gridArrowWidth;
|
891 | break;
|
892 | case GridArrowTypes.leftBottom:
|
893 | contentTop = y - labelHeight;
|
894 | contentLeft = x + gridArrowWidth;
|
895 | break;
|
896 | case GridArrowTypes.rightTop:
|
897 | contentTop = y;
|
898 | contentLeft = x - gridArrowWidth - labelWidth;
|
899 | break;
|
900 | case GridArrowTypes.rightMid:
|
901 | contentTop = y - (labelHeight / 2);
|
902 | contentLeft = x - gridArrowWidth - labelWidth;
|
903 | break;
|
904 | case GridArrowTypes.rightBottom:
|
905 | contentTop = y - labelHeight;
|
906 | contentLeft = x - labelWidth - gridArrowWidth;
|
907 | break;
|
908 | case GridArrowTypes.topLeft:
|
909 | contentTop = y + gridArrowWidth;
|
910 | contentLeft = x;
|
911 | break;
|
912 | case GridArrowTypes.topMid:
|
913 | contentTop = y + gridArrowWidth;
|
914 | contentLeft = x - (labelWidth / 2);
|
915 | break;
|
916 | case GridArrowTypes.topRight:
|
917 | contentTop = y + gridArrowWidth;
|
918 | contentLeft = x - labelWidth;
|
919 | break;
|
920 | case GridArrowTypes.bottomLeft:
|
921 | contentTop = y - gridArrowWidth - labelHeight;
|
922 | contentLeft = x;
|
923 | break;
|
924 | case GridArrowTypes.bottomMid:
|
925 | contentTop = y - gridArrowWidth - labelHeight;
|
926 | contentLeft = x - (labelWidth / 2);
|
927 | break;
|
928 | case GridArrowTypes.bottomRight:
|
929 | contentTop = y - gridArrowWidth - labelHeight;
|
930 | contentLeft = x - labelWidth;
|
931 | break;
|
932 | }
|
933 | return {
|
934 | contentTop,
|
935 | contentLeft,
|
936 | };
|
937 | }
|
938 |
|
939 |
|
940 |
|
941 |
|
942 |
|
943 |
|
944 |
|
945 |
|
946 |
|
947 |
|
948 |
|
949 |
|
950 |
|
951 |
|
952 |
|
953 | export function generateLegibleTextColor(backgroundColor: string) {
|
954 | let rgb: number[] = [];
|
955 |
|
956 |
|
957 | const rgba = parseHexa(backgroundColor + '00');
|
958 | if (rgba.length === 4) {
|
959 | rgb = rgba.slice(0, 3).map(c => c);
|
960 | } else {
|
961 |
|
962 | const parsed = backgroundColor.match(/[0-9.]+/g);
|
963 | if (!parsed) {
|
964 | return null;
|
965 | }
|
966 | rgb = parsed.slice(0, 3).map(s => parseInt(s, 10) / 255);
|
967 | }
|
968 |
|
969 | if (!rgb.length) {
|
970 | return null;
|
971 | }
|
972 |
|
973 | return luminance(rgb) > 0.2 ? defaultLabelTextColor : 'white';
|
974 | }
|