github.com/evanw/esbuild@v0.21.4/internal/css_parser/css_decls_gradient.go (about) 1 package css_parser 2 3 import ( 4 "fmt" 5 "math" 6 "strconv" 7 "strings" 8 9 "github.com/evanw/esbuild/internal/compat" 10 "github.com/evanw/esbuild/internal/css_ast" 11 "github.com/evanw/esbuild/internal/css_lexer" 12 "github.com/evanw/esbuild/internal/helpers" 13 "github.com/evanw/esbuild/internal/logger" 14 ) 15 16 type gradientKind uint8 17 18 const ( 19 linearGradient gradientKind = iota 20 radialGradient 21 conicGradient 22 ) 23 24 type parsedGradient struct { 25 leadingTokens []css_ast.Token 26 colorStops []colorStop 27 kind gradientKind 28 repeating bool 29 } 30 31 type colorStop struct { 32 positions []css_ast.Token 33 color css_ast.Token 34 midpoint css_ast.Token // Absent if "midpoint.Kind == css_lexer.T(0)" 35 } 36 37 func parseGradient(token css_ast.Token) (gradient parsedGradient, success bool) { 38 if token.Kind != css_lexer.TFunction { 39 return 40 } 41 42 switch strings.ToLower(token.Text) { 43 case "linear-gradient": 44 gradient.kind = linearGradient 45 46 case "radial-gradient": 47 gradient.kind = radialGradient 48 49 case "conic-gradient": 50 gradient.kind = conicGradient 51 52 case "repeating-linear-gradient": 53 gradient.kind = linearGradient 54 gradient.repeating = true 55 56 case "repeating-radial-gradient": 57 gradient.kind = radialGradient 58 gradient.repeating = true 59 60 case "repeating-conic-gradient": 61 gradient.kind = conicGradient 62 gradient.repeating = true 63 64 default: 65 return 66 } 67 68 // Bail if any token is a "var()" since it may introduce commas 69 tokens := *token.Children 70 for _, t := range tokens { 71 if t.Kind == css_lexer.TFunction && strings.EqualFold(t.Text, "var") { 72 return 73 } 74 } 75 76 // Try to strip the initial tokens 77 if len(tokens) > 0 && !looksLikeColor(tokens[0]) { 78 i := 0 79 for i < len(tokens) && tokens[i].Kind != css_lexer.TComma { 80 i++ 81 } 82 gradient.leadingTokens = tokens[:i] 83 if i < len(tokens) { 84 tokens = tokens[i+1:] 85 } else { 86 tokens = nil 87 } 88 } 89 90 // Try to parse the color stops 91 for len(tokens) > 0 { 92 // Parse the color 93 color := tokens[0] 94 if !looksLikeColor(color) { 95 return 96 } 97 tokens = tokens[1:] 98 99 // Parse up to two positions 100 var positions []css_ast.Token 101 for len(positions) < 2 && len(tokens) > 0 { 102 position := tokens[0] 103 if position.Kind.IsNumeric() || (position.Kind == css_lexer.TFunction && strings.EqualFold(position.Text, "calc")) { 104 positions = append(positions, position) 105 } else { 106 break 107 } 108 tokens = tokens[1:] 109 } 110 111 // Parse the comma 112 var midpoint css_ast.Token 113 if len(tokens) > 0 { 114 if tokens[0].Kind != css_lexer.TComma { 115 return 116 } 117 tokens = tokens[1:] 118 if len(tokens) == 0 { 119 return 120 } 121 122 // Parse the midpoint, if any 123 if len(tokens) > 0 && tokens[0].Kind.IsNumeric() { 124 midpoint = tokens[0] 125 tokens = tokens[1:] 126 127 // Followed by a mandatory comma 128 if len(tokens) == 0 || tokens[0].Kind != css_lexer.TComma { 129 return 130 } 131 tokens = tokens[1:] 132 } 133 } 134 135 // Add the color stop 136 gradient.colorStops = append(gradient.colorStops, colorStop{ 137 color: color, 138 positions: positions, 139 midpoint: midpoint, 140 }) 141 } 142 143 success = true 144 return 145 } 146 147 func (p *parser) generateGradient(token css_ast.Token, gradient parsedGradient) css_ast.Token { 148 var children []css_ast.Token 149 commaToken := p.commaToken(token.Loc) 150 151 children = append(children, gradient.leadingTokens...) 152 for _, stop := range gradient.colorStops { 153 if len(children) > 0 { 154 children = append(children, commaToken) 155 } 156 if len(stop.positions) == 0 && stop.midpoint.Kind == css_lexer.T(0) { 157 stop.color.Whitespace &= ^css_ast.WhitespaceAfter 158 } 159 children = append(children, stop.color) 160 children = append(children, stop.positions...) 161 if stop.midpoint.Kind != css_lexer.T(0) { 162 children = append(children, commaToken, stop.midpoint) 163 } 164 } 165 166 token.Children = &children 167 return token 168 } 169 170 func (p *parser) lowerAndMinifyGradient(token css_ast.Token, wouldClipColor *bool) css_ast.Token { 171 gradient, ok := parseGradient(token) 172 if !ok { 173 return token 174 } 175 176 lowerMidpoints := p.options.unsupportedCSSFeatures.Has(compat.GradientMidpoints) 177 lowerColorSpaces := p.options.unsupportedCSSFeatures.Has(compat.ColorFunctions) 178 lowerInterpolation := p.options.unsupportedCSSFeatures.Has(compat.GradientInterpolation) 179 180 // Assume that if the browser doesn't support color spaces in gradients, then 181 // it doesn't correctly interpolate non-sRGB colors even when a color space 182 // is not specified. This is the case for Firefox 120, for example, which has 183 // support for the "color()" syntax but not for color spaces in gradients. 184 // There is no entry in our feature support matrix for this edge case so we 185 // make this assumption instead. 186 // 187 // Note that this edge case means we have to _replace_ the original gradient 188 // with the expanded one instead of inserting a fallback before it. Otherwise 189 // Firefox 120 would use the original gradient instead of the fallback because 190 // it supports the syntax, but just renders it incorrectly. 191 if lowerInterpolation { 192 lowerColorSpaces = true 193 } 194 195 // Potentially expand the gradient to handle unsupported features 196 didExpand := false 197 if lowerMidpoints || lowerColorSpaces || lowerInterpolation { 198 if colorStops, ok := tryToParseColorStops(gradient); ok { 199 hasColorSpace := false 200 hasMidpoint := false 201 for _, stop := range colorStops { 202 if stop.hasColorSpace { 203 hasColorSpace = true 204 } 205 if stop.midpoint != nil { 206 hasMidpoint = true 207 } 208 } 209 remaining, colorSpace, hueMethod, hasInterpolation := removeColorInterpolation(gradient.leadingTokens) 210 if (hasInterpolation && lowerInterpolation) || (hasColorSpace && lowerColorSpaces) || (hasMidpoint && lowerMidpoints) { 211 if hasInterpolation { 212 tryToExpandGradient(token.Loc, &gradient, colorStops, remaining, colorSpace, hueMethod) 213 } else { 214 if hasColorSpace { 215 colorSpace = colorSpace_oklab 216 } else { 217 colorSpace = colorSpace_srgb 218 } 219 tryToExpandGradient(token.Loc, &gradient, colorStops, gradient.leadingTokens, colorSpace, shorterHue) 220 } 221 didExpand = true 222 } 223 } 224 } 225 226 // Lower all colors in the gradient stop 227 for i, stop := range gradient.colorStops { 228 gradient.colorStops[i].color = p.lowerAndMinifyColor(stop.color, wouldClipColor) 229 } 230 231 if p.options.unsupportedCSSFeatures.Has(compat.GradientDoublePosition) { 232 // Replace double positions with duplicated single positions 233 for _, stop := range gradient.colorStops { 234 if len(stop.positions) > 1 { 235 gradient.colorStops = switchToSinglePositions(gradient.colorStops) 236 break 237 } 238 } 239 } else if p.options.minifySyntax { 240 // Replace duplicated single positions with double positions 241 for i, stop := range gradient.colorStops { 242 if i > 0 && len(stop.positions) == 1 { 243 if prev := gradient.colorStops[i-1]; len(prev.positions) == 1 && prev.midpoint.Kind == css_lexer.T(0) && 244 css_ast.TokensEqual([]css_ast.Token{prev.color}, []css_ast.Token{stop.color}, nil) { 245 gradient.colorStops = switchToDoublePositions(gradient.colorStops) 246 break 247 } 248 } 249 } 250 } 251 252 if p.options.minifySyntax || didExpand { 253 gradient.colorStops = removeImpliedPositions(gradient.kind, gradient.colorStops) 254 } 255 256 return p.generateGradient(token, gradient) 257 } 258 259 func removeImpliedPositions(kind gradientKind, colorStops []colorStop) []colorStop { 260 if len(colorStops) == 0 { 261 return colorStops 262 } 263 264 positions := make([]valueWithUnit, len(colorStops)) 265 for i, stop := range colorStops { 266 if len(stop.positions) == 1 { 267 if pos, ok := tryToParseValue(stop.positions[0], kind); ok { 268 positions[i] = pos 269 continue 270 } 271 } 272 positions[i].value = helpers.NewF64(math.NaN()) 273 } 274 275 start := 0 276 for start < len(colorStops) { 277 if startPos := positions[start]; !startPos.value.IsNaN() { 278 end := start + 1 279 run: 280 for colorStops[end-1].midpoint.Kind == css_lexer.T(0) && end < len(colorStops) { 281 endPos := positions[end] 282 if endPos.value.IsNaN() || endPos.unit != startPos.unit { 283 break 284 } 285 286 // Check that all values in this run are implied. Interpolation is done 287 // using the start and end positions instead of the first and second 288 // positions because it's more accurate. 289 for i := start + 1; i < end; i++ { 290 t := helpers.NewF64(float64(i - start)).DivConst(float64(end - start)) 291 impliedValue := helpers.Lerp(startPos.value, endPos.value, t) 292 if positions[i].value.Sub(impliedValue).Abs().Value() > 0.01 { 293 break run 294 } 295 } 296 end++ 297 } 298 299 // Clear out all implied values 300 if end-start > 1 { 301 for i := start + 1; i+1 < end; i++ { 302 colorStops[i].positions = nil 303 } 304 start = end - 1 305 continue 306 } 307 } 308 start++ 309 } 310 311 if first := colorStops[0].positions; len(first) == 1 && 312 ((first[0].Kind == css_lexer.TPercentage && first[0].PercentageValue() == "0") || 313 (first[0].Kind == css_lexer.TDimension && first[0].DimensionValue() == "0")) { 314 colorStops[0].positions = nil 315 } 316 317 if last := colorStops[len(colorStops)-1].positions; len(last) == 1 && 318 last[0].Kind == css_lexer.TPercentage && last[0].PercentageValue() == "100" { 319 colorStops[len(colorStops)-1].positions = nil 320 } 321 322 return colorStops 323 } 324 325 func switchToSinglePositions(double []colorStop) (single []colorStop) { 326 for _, stop := range double { 327 for i := range stop.positions { 328 stop.positions[i].Whitespace = css_ast.WhitespaceBefore 329 } 330 for len(stop.positions) > 1 { 331 clone := stop 332 clone.positions = stop.positions[:1] 333 clone.midpoint = css_ast.Token{} 334 single = append(single, clone) 335 stop.positions = stop.positions[1:] 336 } 337 single = append(single, stop) 338 } 339 return 340 } 341 342 func switchToDoublePositions(single []colorStop) (double []colorStop) { 343 for i := 0; i < len(single); i++ { 344 stop := single[i] 345 if i+1 < len(single) && len(stop.positions) == 1 && stop.midpoint.Kind == css_lexer.T(0) { 346 if next := single[i+1]; len(next.positions) == 1 && 347 css_ast.TokensEqual([]css_ast.Token{stop.color}, []css_ast.Token{next.color}, nil) { 348 double = append(double, colorStop{ 349 color: stop.color, 350 positions: []css_ast.Token{stop.positions[0], next.positions[0]}, 351 midpoint: next.midpoint, 352 }) 353 i++ 354 continue 355 } 356 } 357 double = append(double, stop) 358 } 359 return 360 } 361 362 func removeColorInterpolation(tokens []css_ast.Token) ([]css_ast.Token, colorSpace, hueMethod, bool) { 363 for i := 0; i+1 < len(tokens); i++ { 364 if in := tokens[i]; in.Kind == css_lexer.TIdent && strings.EqualFold(in.Text, "in") { 365 if space := tokens[i+1]; space.Kind == css_lexer.TIdent { 366 var colorSpace colorSpace 367 hueMethod := shorterHue 368 start := i 369 end := i + 2 370 371 // Parse the color space 372 switch strings.ToLower(space.Text) { 373 case "a98-rgb": 374 colorSpace = colorSpace_a98_rgb 375 case "display-p3": 376 colorSpace = colorSpace_display_p3 377 case "hsl": 378 colorSpace = colorSpace_hsl 379 case "hwb": 380 colorSpace = colorSpace_hwb 381 case "lab": 382 colorSpace = colorSpace_lab 383 case "lch": 384 colorSpace = colorSpace_lch 385 case "oklab": 386 colorSpace = colorSpace_oklab 387 case "oklch": 388 colorSpace = colorSpace_oklch 389 case "prophoto-rgb": 390 colorSpace = colorSpace_prophoto_rgb 391 case "rec2020": 392 colorSpace = colorSpace_rec2020 393 case "srgb": 394 colorSpace = colorSpace_srgb 395 case "srgb-linear": 396 colorSpace = colorSpace_srgb_linear 397 case "xyz": 398 colorSpace = colorSpace_xyz 399 case "xyz-d50": 400 colorSpace = colorSpace_xyz_d50 401 case "xyz-d65": 402 colorSpace = colorSpace_xyz_d65 403 default: 404 return nil, 0, 0, false 405 } 406 407 // Parse the optional hue mode for polar color spaces 408 if colorSpace.isPolar() && i+3 < len(tokens) { 409 if hue := tokens[i+3]; hue.Kind == css_lexer.TIdent && strings.EqualFold(hue.Text, "hue") { 410 if method := tokens[i+2]; method.Kind == css_lexer.TIdent { 411 switch strings.ToLower(method.Text) { 412 case "shorter": 413 hueMethod = shorterHue 414 case "longer": 415 hueMethod = longerHue 416 case "increasing": 417 hueMethod = increasingHue 418 case "decreasing": 419 hueMethod = decreasingHue 420 default: 421 return nil, 0, 0, false 422 } 423 end = i + 4 424 } 425 } 426 } 427 428 // Remove all parsed tokens 429 remaining := append(append([]css_ast.Token{}, tokens[:start]...), tokens[end:]...) 430 if n := len(remaining); n > 0 { 431 remaining[0].Whitespace &= ^css_ast.WhitespaceBefore 432 remaining[n-1].Whitespace &= ^css_ast.WhitespaceAfter 433 } 434 return remaining, colorSpace, hueMethod, true 435 } 436 } 437 } 438 439 return nil, 0, 0, false 440 } 441 442 type valueWithUnit struct { 443 unit string 444 value F64 445 } 446 447 type parsedColorStop struct { 448 // Position information (may be a sum of two different units) 449 positionTerms []valueWithUnit 450 451 // Color midpoint (a.k.a. transition hint) information 452 midpoint *valueWithUnit 453 454 // Non-premultiplied color information in XYZ space 455 x, y, z, alpha F64 456 457 // Non-premultiplied color information in sRGB space 458 r, g, b F64 459 460 // Premultiplied color information in the interpolation color space 461 v0, v1, v2 F64 462 463 // True if the original color has a color space 464 hasColorSpace bool 465 } 466 467 func tryToParseColorStops(gradient parsedGradient) ([]parsedColorStop, bool) { 468 var colorStops []parsedColorStop 469 470 for _, stop := range gradient.colorStops { 471 color, ok := parseColor(stop.color) 472 if !ok { 473 return nil, false 474 } 475 var r, g, b F64 476 if !color.hasColorSpace { 477 r = helpers.NewF64(float64(hexR(color.hex))).DivConst(255) 478 g = helpers.NewF64(float64(hexG(color.hex))).DivConst(255) 479 b = helpers.NewF64(float64(hexB(color.hex))).DivConst(255) 480 color.x, color.y, color.z = lin_srgb_to_xyz(lin_srgb(r, g, b)) 481 } else { 482 r, g, b = gam_srgb(xyz_to_lin_srgb(color.x, color.y, color.z)) 483 } 484 parsedStop := parsedColorStop{ 485 x: color.x, 486 y: color.y, 487 z: color.z, 488 r: r, 489 g: g, 490 b: b, 491 alpha: helpers.NewF64(float64(hexA(color.hex))).DivConst(255), 492 hasColorSpace: color.hasColorSpace, 493 } 494 495 for i, position := range stop.positions { 496 if position, ok := tryToParseValue(position, gradient.kind); ok { 497 parsedStop.positionTerms = []valueWithUnit{position} 498 } else { 499 return nil, false 500 } 501 502 // Expand double positions 503 if i+1 < len(stop.positions) { 504 colorStops = append(colorStops, parsedStop) 505 } 506 } 507 508 if stop.midpoint.Kind != css_lexer.T(0) { 509 if midpoint, ok := tryToParseValue(stop.midpoint, gradient.kind); ok { 510 parsedStop.midpoint = &midpoint 511 } else { 512 return nil, false 513 } 514 } 515 516 colorStops = append(colorStops, parsedStop) 517 } 518 519 // Automatically fill in missing positions 520 if len(colorStops) > 0 { 521 type stopInfo struct { 522 fromPos valueWithUnit 523 toPos valueWithUnit 524 fromCount int32 525 toCount int32 526 } 527 528 // Fill in missing positions for the endpoints first 529 if first := &colorStops[0]; len(first.positionTerms) == 0 { 530 first.positionTerms = []valueWithUnit{{value: helpers.NewF64(0), unit: "%"}} 531 } 532 if last := &colorStops[len(colorStops)-1]; len(last.positionTerms) == 0 { 533 last.positionTerms = []valueWithUnit{{value: helpers.NewF64(100), unit: "%"}} 534 } 535 536 // Set all positions to be greater than the position before them 537 for i, stop := range colorStops { 538 var prevPos valueWithUnit 539 for j := i - 1; j >= 0; j-- { 540 prev := colorStops[j] 541 if prev.midpoint != nil { 542 prevPos = *prev.midpoint 543 break 544 } 545 if len(prev.positionTerms) == 1 { 546 prevPos = prev.positionTerms[0] 547 break 548 } 549 } 550 if len(stop.positionTerms) == 1 { 551 if prevPos.unit == stop.positionTerms[0].unit { 552 stop.positionTerms[0].value = helpers.Max2(prevPos.value, stop.positionTerms[0].value) 553 } 554 prevPos = stop.positionTerms[0] 555 } 556 if stop.midpoint != nil && prevPos.unit == stop.midpoint.unit { 557 stop.midpoint.value = helpers.Max2(prevPos.value, stop.midpoint.value) 558 } 559 } 560 561 // Scan over all other stops with missing positions 562 infos := make([]stopInfo, len(colorStops)) 563 for i, stop := range colorStops { 564 if len(stop.positionTerms) == 1 { 565 continue 566 } 567 info := &infos[i] 568 569 // Scan backward 570 for from := i - 1; from >= 0; from-- { 571 fromStop := colorStops[from] 572 info.fromCount++ 573 if fromStop.midpoint != nil { 574 info.fromPos = *fromStop.midpoint 575 break 576 } 577 if len(fromStop.positionTerms) == 1 { 578 info.fromPos = fromStop.positionTerms[0] 579 break 580 } 581 } 582 583 // Scan forward 584 for to := i; to < len(colorStops); to++ { 585 info.toCount++ 586 if toStop := colorStops[to]; toStop.midpoint != nil { 587 info.toPos = *toStop.midpoint 588 break 589 } 590 if to+1 < len(colorStops) { 591 if toStop := colorStops[to+1]; len(toStop.positionTerms) == 1 { 592 info.toPos = toStop.positionTerms[0] 593 break 594 } 595 } 596 } 597 } 598 599 // Then fill in all other missing positions 600 for i, stop := range colorStops { 601 if len(stop.positionTerms) != 1 { 602 info := infos[i] 603 t := helpers.NewF64(float64(info.fromCount)).DivConst(float64(info.fromCount + info.toCount)) 604 if info.fromPos.unit == info.toPos.unit { 605 colorStops[i].positionTerms = []valueWithUnit{{ 606 value: helpers.Lerp(info.fromPos.value, info.toPos.value, t), 607 unit: info.fromPos.unit, 608 }} 609 } else { 610 colorStops[i].positionTerms = []valueWithUnit{{ 611 value: t.Neg().AddConst(1).Mul(info.fromPos.value), 612 unit: info.fromPos.unit, 613 }, { 614 value: t.Mul(info.toPos.value), 615 unit: info.toPos.unit, 616 }} 617 } 618 } 619 } 620 621 // Midpoints are only supported if they use the same units as their neighbors 622 for i, stop := range colorStops { 623 if stop.midpoint != nil { 624 next := colorStops[i+1] 625 if len(stop.positionTerms) != 1 || stop.midpoint.unit != stop.positionTerms[0].unit || 626 len(next.positionTerms) != 1 || stop.midpoint.unit != next.positionTerms[0].unit { 627 return nil, false 628 } 629 } 630 } 631 } 632 633 return colorStops, true 634 } 635 636 func tryToParseValue(token css_ast.Token, kind gradientKind) (result valueWithUnit, success bool) { 637 if kind == conicGradient { 638 // <angle-percentage> 639 switch token.Kind { 640 case css_lexer.TDimension: 641 degrees, ok := degreesForAngle(token) 642 if !ok { 643 return 644 } 645 result.value = helpers.NewF64(degrees).MulConst(100.0 / 360) 646 result.unit = "%" 647 648 case css_lexer.TPercentage: 649 percent, err := strconv.ParseFloat(token.PercentageValue(), 64) 650 if err != nil { 651 return 652 } 653 result.value = helpers.NewF64(percent) 654 result.unit = "%" 655 656 default: 657 return 658 } 659 } else { 660 // <length-percentage> 661 switch token.Kind { 662 case css_lexer.TNumber: 663 zero, err := strconv.ParseFloat(token.Text, 64) 664 if err != nil || zero != 0 { 665 return 666 } 667 result.value = helpers.NewF64(0) 668 result.unit = "%" 669 670 case css_lexer.TDimension: 671 dimensionValue, err := strconv.ParseFloat(token.DimensionValue(), 64) 672 if err != nil { 673 return 674 } 675 result.value = helpers.NewF64(dimensionValue) 676 result.unit = token.DimensionUnit() 677 678 case css_lexer.TPercentage: 679 percentageValue, err := strconv.ParseFloat(token.PercentageValue(), 64) 680 if err != nil { 681 return 682 } 683 result.value = helpers.NewF64(percentageValue) 684 result.unit = "%" 685 686 default: 687 return 688 } 689 } 690 691 success = true 692 return 693 } 694 695 func tryToExpandGradient( 696 loc logger.Loc, 697 gradient *parsedGradient, 698 colorStops []parsedColorStop, 699 remaining []css_ast.Token, 700 colorSpace colorSpace, 701 hueMethod hueMethod, 702 ) bool { 703 // Convert color stops into the interpolation color space 704 for i := range colorStops { 705 stop := &colorStops[i] 706 v0, v1, v2 := xyz_to_colorSpace(stop.x, stop.y, stop.z, colorSpace) 707 stop.v0, stop.v1, stop.v2 = premultiply(v0, v1, v2, stop.alpha, colorSpace) 708 } 709 710 // Duplicate the endpoints if they should wrap around to themselves 711 if hueMethod == longerHue && colorSpace.isPolar() && len(colorStops) > 0 { 712 if first := colorStops[0]; len(first.positionTerms) == 1 { 713 if first.positionTerms[0].value.Value() < 0 { 714 colorStops[0].positionTerms[0].value = helpers.NewF64(0) 715 } else if first.positionTerms[0].value.Value() > 0 { 716 first.midpoint = nil 717 first.positionTerms = []valueWithUnit{{value: helpers.NewF64(0), unit: first.positionTerms[0].unit}} 718 colorStops = append([]parsedColorStop{first}, colorStops...) 719 } 720 } 721 if last := colorStops[len(colorStops)-1]; len(last.positionTerms) == 1 { 722 if last.positionTerms[0].unit != "%" || last.positionTerms[0].value.Value() < 100 { 723 last.positionTerms = []valueWithUnit{{value: helpers.NewF64(100), unit: "%"}} 724 colorStops = append(colorStops, last) 725 } 726 } 727 } 728 729 var newColorStops []colorStop 730 var generateColorStops func( 731 int, parsedColorStop, parsedColorStop, 732 F64, F64, F64, F64, F64, F64, F64, F64, 733 F64, F64, F64, F64, F64, F64, F64, F64, 734 ) 735 736 generateColorStops = func( 737 depth int, 738 from parsedColorStop, to parsedColorStop, 739 prevX, prevY, prevZ, prevR, prevG, prevB, prevA, prevT F64, 740 nextX, nextY, nextZ, nextR, nextG, nextB, nextA, nextT F64, 741 ) { 742 if depth > 4 { 743 return 744 } 745 746 t := prevT.Add(nextT).DivConst(2) 747 positionT := t 748 749 // Handle midpoints (which we have already checked uses the same units) 750 if from.midpoint != nil { 751 fromPos := from.positionTerms[0].value 752 toPos := to.positionTerms[0].value 753 stopPos := helpers.Lerp(fromPos, toPos, t) 754 H := from.midpoint.value.Sub(fromPos).Div(toPos.Sub(fromPos)) 755 P := stopPos.Sub(fromPos).Div(toPos.Sub(fromPos)) 756 if H.Value() <= 0 { 757 positionT = helpers.NewF64(1) 758 } else if H.Value() >= 1 { 759 positionT = helpers.NewF64(0) 760 } else { 761 positionT = P.Pow(helpers.NewF64(-1).Div(H.Log2())) 762 } 763 } 764 765 v0, v1, v2 := interpolateColors(from.v0, from.v1, from.v2, to.v0, to.v1, to.v2, colorSpace, hueMethod, positionT) 766 a := helpers.Lerp(from.alpha, to.alpha, positionT) 767 v0, v1, v2 = unpremultiply(v0, v1, v2, a, colorSpace) 768 x, y, z := colorSpace_to_xyz(v0, v1, v2, colorSpace) 769 770 // Stop when the color is similar enough to the sRGB midpoint 771 const epsilon = 4.0 / 255 772 r, g, b := gam_srgb(xyz_to_lin_srgb(x, y, z)) 773 dr := r.Mul(a).Sub(prevR.Mul(prevA).Add(nextR.Mul(nextA)).DivConst(2)) 774 dg := g.Mul(a).Sub(prevG.Mul(prevA).Add(nextG.Mul(nextA)).DivConst(2)) 775 db := b.Mul(a).Sub(prevB.Mul(prevA).Add(nextB.Mul(nextA)).DivConst(2)) 776 if d := dr.Squared().Add(dg.Squared()).Add(db.Squared()); d.Value() < epsilon*epsilon { 777 return 778 } 779 780 // Recursive split before this stop 781 generateColorStops(depth+1, from, to, 782 prevX, prevY, prevZ, prevR, prevG, prevB, prevA, prevT, 783 x, y, z, r, g, b, a, t) 784 785 // Generate this stop 786 color := makeColorToken(loc, x, y, z, a) 787 positionTerms := interpolatePositions(from.positionTerms, to.positionTerms, t) 788 position := makePositionToken(loc, positionTerms) 789 position.Whitespace = css_ast.WhitespaceBefore 790 newColorStops = append(newColorStops, colorStop{ 791 color: color, 792 positions: []css_ast.Token{position}, 793 }) 794 795 // Recursive split after this stop 796 generateColorStops(depth+1, from, to, 797 x, y, z, r, g, b, a, t, 798 nextX, nextY, nextZ, nextR, nextG, nextB, nextA, nextT) 799 } 800 801 for i, stop := range colorStops { 802 color := makeColorToken(loc, stop.x, stop.y, stop.z, stop.alpha) 803 position := makePositionToken(loc, stop.positionTerms) 804 position.Whitespace = css_ast.WhitespaceBefore 805 newColorStops = append(newColorStops, colorStop{ 806 color: color, 807 positions: []css_ast.Token{position}, 808 }) 809 810 // Generate new color stops in between as needed 811 if i+1 < len(colorStops) { 812 next := colorStops[i+1] 813 generateColorStops(0, stop, next, 814 stop.x, stop.y, stop.z, stop.r, stop.g, stop.b, stop.alpha, helpers.NewF64(0), 815 next.x, next.y, next.z, next.r, next.g, next.b, next.alpha, helpers.NewF64(1)) 816 } 817 } 818 819 gradient.leadingTokens = remaining 820 gradient.colorStops = newColorStops 821 return true 822 } 823 824 func formatFloat(value F64, decimals int) string { 825 return strings.TrimSuffix(strings.TrimRight(strconv.FormatFloat(value.Value(), 'f', decimals, 64), "0"), ".") 826 } 827 828 func makeDimensionOrPercentToken(loc logger.Loc, value F64, unit string) (token css_ast.Token) { 829 token.Loc = loc 830 token.Text = formatFloat(value, 2) 831 if unit == "%" { 832 token.Kind = css_lexer.TPercentage 833 } else { 834 token.Kind = css_lexer.TDimension 835 token.UnitOffset = uint16(len(token.Text)) 836 } 837 token.Text += unit 838 return 839 } 840 841 func makePositionToken(loc logger.Loc, positionTerms []valueWithUnit) css_ast.Token { 842 if len(positionTerms) == 1 { 843 return makeDimensionOrPercentToken(loc, positionTerms[0].value, positionTerms[0].unit) 844 } 845 846 children := make([]css_ast.Token, 0, 1+2*len(positionTerms)) 847 for i, term := range positionTerms { 848 if i > 0 { 849 children = append(children, css_ast.Token{ 850 Loc: loc, 851 Kind: css_lexer.TDelimPlus, 852 Text: "+", 853 Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter, 854 }) 855 } 856 children = append(children, makeDimensionOrPercentToken(loc, term.value, term.unit)) 857 } 858 859 return css_ast.Token{ 860 Loc: loc, 861 Kind: css_lexer.TFunction, 862 Text: "calc", 863 Children: &children, 864 } 865 } 866 867 func makeColorToken(loc logger.Loc, x F64, y F64, z F64, a F64) (color css_ast.Token) { 868 color.Loc = loc 869 alpha := uint32(a.MulConst(255).Round().Value()) 870 if hex, ok := tryToConvertToHexWithoutClipping(x, y, z, alpha); ok { 871 color.Kind = css_lexer.THash 872 if alpha == 255 { 873 color.Text = fmt.Sprintf("%06x", hex>>8) 874 } else { 875 color.Text = fmt.Sprintf("%08x", hex) 876 } 877 } else { 878 children := []css_ast.Token{ 879 { 880 Loc: loc, 881 Kind: css_lexer.TIdent, 882 Text: "xyz", 883 Whitespace: css_ast.WhitespaceAfter, 884 }, 885 { 886 Loc: loc, 887 Kind: css_lexer.TNumber, 888 Text: formatFloat(x, 3), 889 Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter, 890 }, 891 { 892 Loc: loc, 893 Kind: css_lexer.TNumber, 894 Text: formatFloat(y, 3), 895 Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter, 896 }, 897 { 898 Loc: loc, 899 Kind: css_lexer.TNumber, 900 Text: formatFloat(z, 3), 901 Whitespace: css_ast.WhitespaceBefore, 902 }, 903 } 904 if a.Value() < 1 { 905 children = append(children, 906 css_ast.Token{ 907 Loc: loc, 908 Kind: css_lexer.TDelimSlash, 909 Text: "/", 910 Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter, 911 }, 912 css_ast.Token{ 913 Loc: loc, 914 Kind: css_lexer.TNumber, 915 Text: formatFloat(a, 3), 916 Whitespace: css_ast.WhitespaceBefore, 917 }, 918 ) 919 } 920 color.Kind = css_lexer.TFunction 921 color.Text = "color" 922 color.Children = &children 923 } 924 return 925 } 926 927 func interpolateHues(a, b, t F64, hueMethod hueMethod) F64 { 928 a = a.DivConst(360) 929 b = b.DivConst(360) 930 a = a.Sub(a.Floor()) 931 b = b.Sub(b.Floor()) 932 933 switch hueMethod { 934 case shorterHue: 935 delta := b.Sub(a) 936 if delta.Value() > 0.5 { 937 a = a.AddConst(1) 938 } 939 if delta.Value() < -0.5 { 940 b = b.AddConst(1) 941 } 942 943 case longerHue: 944 delta := b.Sub(a) 945 if delta.Value() > 0 && delta.Value() < 0.5 { 946 a = a.AddConst(1) 947 } 948 if delta.Value() > -0.5 && delta.Value() <= 0 { 949 b = b.AddConst(1) 950 } 951 952 case increasingHue: 953 if b.Value() < a.Value() { 954 b = b.AddConst(1) 955 } 956 957 case decreasingHue: 958 if a.Value() < b.Value() { 959 a = a.AddConst(1) 960 } 961 } 962 963 return helpers.Lerp(a, b, t).MulConst(360) 964 } 965 966 func interpolateColors( 967 a0, a1, a2 F64, b0, b1, b2 F64, 968 colorSpace colorSpace, hueMethod hueMethod, t F64, 969 ) (v0 F64, v1 F64, v2 F64) { 970 v1 = helpers.Lerp(a1, b1, t) 971 972 switch colorSpace { 973 case colorSpace_hsl, colorSpace_hwb: 974 v2 = helpers.Lerp(a2, b2, t) 975 v0 = interpolateHues(a0, b0, t, hueMethod) 976 977 case colorSpace_lch, colorSpace_oklch: 978 v0 = helpers.Lerp(a0, b0, t) 979 v2 = interpolateHues(a2, b2, t, hueMethod) 980 981 default: 982 v0 = helpers.Lerp(a0, b0, t) 983 v2 = helpers.Lerp(a2, b2, t) 984 } 985 986 return v0, v1, v2 987 } 988 989 func interpolatePositions(a []valueWithUnit, b []valueWithUnit, t F64) (result []valueWithUnit) { 990 findUnit := func(unit string) int { 991 for i, x := range result { 992 if x.unit == unit { 993 return i 994 } 995 } 996 result = append(result, valueWithUnit{unit: unit}) 997 return len(result) - 1 998 } 999 1000 // "result += a * (1 - t)" 1001 for _, term := range a { 1002 ptr := &result[findUnit(term.unit)] 1003 ptr.value = t.Neg().AddConst(1).Mul(term.value).Add(ptr.value) 1004 } 1005 1006 // "result += b * t" 1007 for _, term := range b { 1008 ptr := &result[findUnit(term.unit)] 1009 ptr.value = t.Mul(term.value).Add(ptr.value) 1010 } 1011 1012 // Remove an extra zero value for neatness. We don't remove all 1013 // of them because it may be important to retain a single zero. 1014 if len(result) > 1 { 1015 for i, term := range result { 1016 if term.value.Value() == 0 { 1017 copy(result[i:], result[i+1:]) 1018 result = result[:len(result)-1] 1019 break 1020 } 1021 } 1022 } 1023 1024 return 1025 } 1026 1027 func premultiply(v0, v1, v2, alpha F64, colorSpace colorSpace) (F64, F64, F64) { 1028 if alpha.Value() < 1 { 1029 switch colorSpace { 1030 case colorSpace_hsl, colorSpace_hwb: 1031 v2 = v2.Mul(alpha) 1032 case colorSpace_lch, colorSpace_oklch: 1033 v0 = v0.Mul(alpha) 1034 default: 1035 v0 = v0.Mul(alpha) 1036 v2 = v2.Mul(alpha) 1037 } 1038 v1 = v1.Mul(alpha) 1039 } 1040 return v0, v1, v2 1041 } 1042 1043 func unpremultiply(v0, v1, v2, alpha F64, colorSpace colorSpace) (F64, F64, F64) { 1044 if alpha.Value() > 0 && alpha.Value() < 1 { 1045 switch colorSpace { 1046 case colorSpace_hsl, colorSpace_hwb: 1047 v2 = v2.Div(alpha) 1048 case colorSpace_lch, colorSpace_oklch: 1049 v0 = v0.Div(alpha) 1050 default: 1051 v0 = v0.Div(alpha) 1052 v2 = v2.Div(alpha) 1053 } 1054 v1 = v1.Div(alpha) 1055 } 1056 return v0, v1, v2 1057 }