1// Copyright 2020 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// Copyright (C) 2012 Google Inc. All rights reserved. 6 7// Redistribution and use in source and binary forms, with or without 8// modification, are permitted provided that the following conditions 9// are met: 10 11// 1. Redistributions of source code must retain the above copyright 12// notice, this list of conditions and the following disclaimer. 13// 2. Redistributions in binary form must reproduce the above copyright 14// notice, this list of conditions and the following disclaimer in the 15// documentation and/or other materials provided with the distribution. 16// 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 17// its contributors may be used to endorse or promote products derived 18// from this software without specific prior written permission. 19 20// THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 21// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23// DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 24// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 29// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 31 32import {AreaBounds, Bounds} from './common.js'; 33import {drawGridLabels, GridLabelState} from './css_grid_label_helpers.js'; 34import {applyMatrixToPoint, buildPath, emptyBounds} from './highlight_common.js'; 35 36const DEFAULT_EXTENDED_LINE_COLOR = 'rgba(128, 128, 128, 0.3)'; 37 38// TODO(alexrudenko): Grid label unit tests depend on this style so it cannot be extracted yet. 39export const gridStyle = ` 40/* Grid row and column labels */ 41.grid-label-content { 42 position: absolute; 43 -webkit-user-select: none; 44 padding: 2px; 45 font-family: Menlo, monospace; 46 font-size: 10px; 47 min-width: 17px; 48 min-height: 15px; 49 border-radius: 2px; 50 box-sizing: border-box; 51 z-index: 1; 52 background-clip: padding-box; 53 pointer-events: none; 54 text-align: center; 55 display: flex; 56 justify-content: center; 57 align-items: center; 58} 59 60.grid-label-content[data-direction=row] { 61 background-color: var(--row-label-color, #1A73E8); 62 color: #121212; 63} 64 65.grid-label-content[data-direction=column] { 66 background-color: var(--column-label-color, #1A73E8); 67 color: #121212; 68} 69 70.line-names ul, 71.line-names .line-name { 72 margin: 0; 73 padding: 0; 74 list-style: none; 75} 76 77.line-names .line-name { 78 max-width: 100px; 79 white-space: nowrap; 80 overflow: hidden; 81 text-overflow: ellipsis; 82} 83 84.line-names .grid-label-content, 85.line-numbers .grid-label-content, 86.track-sizes .grid-label-content { 87 border: 1px solid white; 88 --inner-corner-avoid-distance: 15px; 89} 90 91.grid-label-content.top-left.inner-shared-corner, 92.grid-label-content.top-right.inner-shared-corner { 93 transform: translateY(var(--inner-corner-avoid-distance)); 94} 95 96.grid-label-content.bottom-left.inner-shared-corner, 97.grid-label-content.bottom-right.inner-shared-corner { 98 transform: translateY(calc(var(--inner-corner-avoid-distance) * -1)); 99} 100 101.grid-label-content.left-top.inner-shared-corner, 102.grid-label-content.left-bottom.inner-shared-corner { 103 transform: translateX(var(--inner-corner-avoid-distance)); 104} 105 106.grid-label-content.right-top.inner-shared-corner, 107.grid-label-content.right-bottom.inner-shared-corner { 108 transform: translateX(calc(var(--inner-corner-avoid-distance) * -1)); 109} 110 111.line-names .grid-label-content::before, 112.line-numbers .grid-label-content::before, 113.track-sizes .grid-label-content::before { 114 position: absolute; 115 z-index: 1; 116 pointer-events: none; 117 content: ""; 118 width: 3px; 119 height: 3px; 120 border: 1px solid white; 121 border-width: 0 1px 1px 0; 122} 123 124.line-names .grid-label-content[data-direction=row]::before, 125.line-numbers .grid-label-content[data-direction=row]::before, 126.track-sizes .grid-label-content[data-direction=row]::before { 127 background: var(--row-label-color, #1A73E8); 128} 129 130.line-names .grid-label-content[data-direction=column]::before, 131.line-numbers .grid-label-content[data-direction=column]::before, 132.track-sizes .grid-label-content[data-direction=column]::before { 133 background: var(--column-label-color, #1A73E8); 134} 135 136.grid-label-content.bottom-mid::before { 137 transform: translateY(-1px) rotate(45deg); 138 top: 100%; 139} 140 141.grid-label-content.top-mid::before { 142 transform: translateY(-3px) rotate(-135deg); 143 top: 0%; 144} 145 146.grid-label-content.left-mid::before { 147 transform: translateX(-3px) rotate(135deg); 148 left: 0% 149} 150 151.grid-label-content.right-mid::before { 152 transform: translateX(3px) rotate(-45deg); 153 right: 0%; 154} 155 156.grid-label-content.right-top::before { 157 transform: translateX(3px) translateY(-1px) rotate(-90deg) skewY(30deg); 158 right: 0%; 159 top: 0%; 160} 161 162.grid-label-content.right-bottom::before { 163 transform: translateX(3px) translateY(-3px) skewX(30deg); 164 right: 0%; 165 top: 100%; 166} 167 168.grid-label-content.bottom-right::before { 169 transform: translateX(1px) translateY(-1px) skewY(30deg); 170 right: 0%; 171 top: 100%; 172} 173 174.grid-label-content.bottom-left::before { 175 transform: translateX(-1px) translateY(-1px) rotate(90deg) skewX(30deg); 176 left: 0%; 177 top: 100%; 178} 179 180.grid-label-content.left-top::before { 181 transform: translateX(-3px) translateY(-1px) rotate(180deg) skewX(30deg); 182 left: 0%; 183 top: 0%; 184} 185 186.grid-label-content.left-bottom::before { 187 transform: translateX(-3px) translateY(-3px) rotate(90deg) skewY(30deg); 188 left: 0%; 189 top: 100%; 190} 191 192.grid-label-content.top-right::before { 193 transform: translateX(1px) translateY(-3px) rotate(-90deg) skewX(30deg); 194 right: 0%; 195 top: 0%; 196} 197 198.grid-label-content.top-left::before { 199 transform: translateX(-1px) translateY(-3px) rotate(180deg) skewY(30deg); 200 left: 0%; 201 top: 0%; 202} 203 204@media (forced-colors: active) { 205 .grid-label-content { 206 border-color: Highlight; 207 background-color: Canvas; 208 color: Text; 209 forced-color-adjust: none; 210 } 211 .grid-label-content::before { 212 background-color: Canvas; 213 border-color: Highlight; 214 } 215}`; 216 217export interface GridHighlight { 218 gridBorder: Array<string|number>; 219 writingMode: string; 220 rowGaps: Array<string|number>; 221 rotationAngle: number, columnGaps: Array<string|number>; 222 rows: Array<string|number>; 223 columns: Array<string|number>; 224 areaNames: {[key: string]: Array<string|number>}; 225 gridHighlightConfig: { 226 gridBackgroundColor?: string; 227 gridBorderColor?: string; 228 rowGapColor?: string; 229 columnGapColor?: string; 230 areaBorderColor?: string; gridBorderDash: boolean; rowLineDash: boolean; columnLineDash: boolean; 231 showGridExtensionLines: boolean; 232 showPositiveLineNumbers: boolean; 233 showNegativeLineNumbers: boolean; 234 rowLineColor: string; 235 columnLineColor: string; 236 rowHatchColor: string; 237 columnHatchColor: string; 238 showLineNames: boolean; 239 } 240} 241 242export function drawLayoutGridHighlight( 243 highlight: GridHighlight, context: CanvasRenderingContext2D, deviceScaleFactor: number, canvasWidth: number, 244 canvasHeight: number, emulationScaleFactor: number, labelState: GridLabelState) { 245 const gridBounds = emptyBounds(); 246 const gridPath = buildPath(highlight.gridBorder, gridBounds, emulationScaleFactor); 247 248 // Transform the context to match the current writing-mode. 249 context.save(); 250 _applyWritingModeTransformation(highlight.writingMode, gridBounds, context); 251 252 // Draw grid background 253 if (highlight.gridHighlightConfig.gridBackgroundColor) { 254 context.fillStyle = highlight.gridHighlightConfig.gridBackgroundColor; 255 context.fill(gridPath); 256 } 257 258 // Draw Grid border 259 if (highlight.gridHighlightConfig.gridBorderColor) { 260 context.save(); 261 context.translate(0.5, 0.5); 262 context.lineWidth = 0; 263 if (highlight.gridHighlightConfig.gridBorderDash) { 264 context.setLineDash([3, 3]); 265 } 266 context.strokeStyle = highlight.gridHighlightConfig.gridBorderColor; 267 context.stroke(gridPath); 268 context.restore(); 269 } 270 271 // Draw grid lines 272 const rowBounds = _drawGridLines(context, highlight, 'row', emulationScaleFactor); 273 const columnBounds = _drawGridLines(context, highlight, 'column', emulationScaleFactor); 274 275 // Draw gaps 276 _drawGridGap( 277 context, highlight.rowGaps, highlight.gridHighlightConfig.rowGapColor, 278 highlight.gridHighlightConfig.rowHatchColor, highlight.rotationAngle, emulationScaleFactor, 279 /* flipDirection */ true); 280 _drawGridGap( 281 context, highlight.columnGaps, highlight.gridHighlightConfig.columnGapColor, 282 highlight.gridHighlightConfig.columnHatchColor, highlight.rotationAngle, emulationScaleFactor, 283 /* flipDirection */ false); 284 285 // Draw named grid areas 286 const areaBounds = 287 _drawGridAreas(context, highlight.areaNames, highlight.gridHighlightConfig.areaBorderColor, emulationScaleFactor); 288 289 // The rest of the overlay is drawn without the writing-mode transformation, but we keep the matrix to transform relevant points. 290 const writingModeMatrix = context.getTransform(); 291 writingModeMatrix.scaleSelf(1 / deviceScaleFactor); 292 context.restore(); 293 294 if (highlight.gridHighlightConfig.showGridExtensionLines) { 295 if (rowBounds) { 296 _drawExtendedGridLines( 297 context, rowBounds, highlight.gridHighlightConfig.rowLineDash, writingModeMatrix, canvasWidth, canvasHeight); 298 } 299 if (columnBounds) { 300 _drawExtendedGridLines( 301 context, columnBounds, highlight.gridHighlightConfig.columnLineDash, writingModeMatrix, canvasWidth, 302 canvasHeight); 303 } 304 } 305 306 // Draw all the labels 307 drawGridLabels(highlight, gridBounds, areaBounds, {canvasWidth, canvasHeight}, labelState, writingModeMatrix); 308} 309 310function _applyWritingModeTransformation(writingMode: string, gridBounds: Bounds, context: CanvasRenderingContext2D) { 311 if (writingMode !== 'vertical-rl' && writingMode !== 'vertical-lr') { 312 return; 313 } 314 315 const topLeft = gridBounds.allPoints[0]; 316 const bottomLeft = gridBounds.allPoints[3]; 317 318 // Move to the top-left corner to do all transformations there. 319 context.translate(topLeft.x, topLeft.y); 320 321 if (writingMode === 'vertical-rl') { 322 context.rotate(90 * Math.PI / 180); 323 context.translate(0, -1 * (bottomLeft.y - topLeft.y)); 324 } 325 326 if (writingMode === 'vertical-lr') { 327 context.rotate(90 * Math.PI / 180); 328 context.scale(1, -1); 329 } 330 331 // Move back to the original point. 332 context.translate(topLeft.x * -1, topLeft.y * -1); 333} 334 335function _drawGridLines( 336 context: CanvasRenderingContext2D, highlight: GridHighlight, direction: 'row'|'column', 337 emulationScaleFactor: number) { 338 const tracks = highlight[`${direction}s` as 'rows' | 'columns']; 339 const color = highlight.gridHighlightConfig[`${direction}LineColor` as 'rowLineColor' | 'columnLineColor']; 340 const dash = highlight.gridHighlightConfig[`${direction}LineDash` as 'rowLineDash' | 'columnLineDash']; 341 342 if (!color) { 343 return null; 344 } 345 346 const bounds = emptyBounds(); 347 const path = buildPath(tracks, bounds, emulationScaleFactor); 348 349 context.save(); 350 context.translate(0.5, 0.5); 351 if (dash) { 352 context.setLineDash([3, 3]); 353 } 354 context.lineWidth = 0; 355 context.strokeStyle = color; 356 357 context.save(); 358 context.stroke(path); 359 context.restore(); 360 361 context.restore(); 362 363 return bounds; 364} 365 366function _drawExtendedGridLines( 367 context: CanvasRenderingContext2D, bounds: Bounds, dash: boolean|undefined, writingModeMatrix: DOMMatrix, 368 canvasWidth: number, canvasHeight: number) { 369 context.save(); 370 context.strokeStyle = DEFAULT_EXTENDED_LINE_COLOR; 371 context.lineWidth = 1; 372 context.translate(0.5, 0.5); 373 if (dash) { 374 context.setLineDash([3, 3]); 375 } 376 377 // A grid track path is a list of lines defined by 2 points. 378 // Here we're going through the list of all points 2 by 2, so we can draw the extensions at the edges of each line. 379 for (let i = 0; i < bounds.allPoints.length; i += 2) { 380 let point1 = applyMatrixToPoint(bounds.allPoints[i], writingModeMatrix); 381 let point2 = applyMatrixToPoint(bounds.allPoints[i + 1], writingModeMatrix); 382 let edgePoint1; 383 let edgePoint2; 384 385 if (point1.x === point2.x) { 386 // Special case for a vertical line. 387 edgePoint1 = {x: point1.x, y: 0}; 388 edgePoint2 = {x: point1.x, y: canvasHeight}; 389 if (point2.y < point1.y) { 390 [point1, point2] = [point2, point1]; 391 } 392 } else if (point1.y === point2.y) { 393 // Special case for a horizontal line. 394 edgePoint1 = {x: 0, y: point1.y}; 395 edgePoint2 = {x: canvasWidth, y: point1.y}; 396 if (point2.x < point1.x) { 397 [point1, point2] = [point2, point1]; 398 } 399 } else { 400 // When the line isn't straight, we need to do some maths. 401 const a = (point2.y - point1.y) / (point2.x - point1.x); 402 const b = (point1.y * point2.x - point2.y * point1.x) / (point2.x - point1.x); 403 404 edgePoint1 = {x: 0, y: b}; 405 edgePoint2 = {x: canvasWidth, y: (canvasWidth * a) + b}; 406 407 if (point2.x < point1.x) { 408 [point1, point2] = [point2, point1]; 409 } 410 } 411 412 context.beginPath(); 413 context.moveTo(edgePoint1.x, edgePoint1.y); 414 context.lineTo(point1.x, point1.y); 415 context.moveTo(point2.x, point2.y); 416 context.lineTo(edgePoint2.x, edgePoint2.y); 417 context.stroke(); 418 } 419 420 context.restore(); 421} 422 423/** 424 * Draw all of the named grid area paths. This does not draw the labels, as 425 * placing labels in and around the grid for various things is handled later. 426 */ 427function _drawGridAreas( 428 context: CanvasRenderingContext2D, areas: {[key: string]: Array<string|number>}, borderColor: string|undefined, 429 emulationScaleFactor: number): AreaBounds[] { 430 if (!areas || !Object.keys(areas).length) { 431 return []; 432 } 433 434 context.save(); 435 if (borderColor) { 436 context.strokeStyle = borderColor; 437 } 438 context.lineWidth = 2; 439 440 const areaBounds = []; 441 442 for (const name in areas) { 443 const areaCommands = areas[name]; 444 445 const bounds = emptyBounds(); 446 const path = buildPath(areaCommands, bounds, emulationScaleFactor); 447 448 context.stroke(path); 449 450 areaBounds.push({name, bounds}); 451 } 452 453 context.restore(); 454 455 return areaBounds; 456} 457 458function _drawGridGap( 459 context: CanvasRenderingContext2D, gapCommands: Array<number|string>, gapColor: string|undefined, 460 hatchColor: string|undefined, rotationAngle: number, emulationScaleFactor: number, 461 flipDirection: boolean|undefined) { 462 if (!gapColor && !hatchColor) { 463 return; 464 } 465 466 context.save(); 467 context.translate(0.5, 0.5); 468 context.lineWidth = 0; 469 470 const bounds = emptyBounds(); 471 const path = buildPath(gapCommands, bounds, emulationScaleFactor); 472 473 // Fill the gap background if needed. 474 if (gapColor) { 475 context.fillStyle = gapColor; 476 context.fill(path); 477 } 478 479 // And draw the hatch pattern if needed. 480 if (hatchColor) { 481 _hatchFillPath(context, path, bounds, /* delta */ 10, hatchColor, rotationAngle, flipDirection); 482 } 483 context.restore(); 484} 485 486/** 487 * Draw line hatching at a 45 degree angle for a given 488 * path. 489 * __________ 490 * |\ \ \ | 491 * | \ \ \| 492 * | \ \ | 493 * |\ \ \ | 494 * ********** 495 */ 496function _hatchFillPath( 497 context: CanvasRenderingContext2D, path: Path2D, bounds: Bounds, delta: number, color: string, 498 rotationAngle: number, flipDirection: boolean|undefined) { 499 const dx = bounds.maxX - bounds.minX; 500 const dy = bounds.maxY - bounds.minY; 501 context.rect(bounds.minX, bounds.minY, dx, dy); 502 context.save(); 503 context.clip(path); 504 context.setLineDash([5, 3]); 505 const majorAxis = Math.max(dx, dy); 506 context.strokeStyle = color; 507 const centerX = bounds.minX + dx / 2; 508 const centerY = bounds.minY + dy / 2; 509 context.translate(centerX, centerY); 510 context.rotate(rotationAngle * Math.PI / 180); 511 context.translate(-centerX, -centerY); 512 if (flipDirection) { 513 for (let i = -majorAxis; i < majorAxis; i += delta) { 514 context.beginPath(); 515 context.moveTo(bounds.maxX - i, bounds.minY); 516 context.lineTo(bounds.maxX - dy - i, bounds.maxY); 517 context.stroke(); 518 } 519 } else { 520 for (let i = -majorAxis; i < majorAxis; i += delta) { 521 context.beginPath(); 522 context.moveTo(i + bounds.minX, bounds.minY); 523 context.lineTo(dy + i + bounds.minX, bounds.maxY); 524 context.stroke(); 525 } 526 } 527 context.restore(); 528} 529