css: implement lowering of gradient syntax

This commit is contained in:
Evan Wallace
2023-12-09 23:04:23 -05:00
parent 4a3b265608
commit 83b4171725
11 changed files with 1770 additions and 88 deletions
+91 -2
View File
@@ -2,9 +2,98 @@
## Unreleased
* Add support for transforming new CSS gradient syntax for older browsers
The specification called [CSS Images Module Level 4](https://www.w3.org/TR/css-images-4/) introduces new CSS gradient syntax for customizing how the browser interpolates colors in between color stops. You can now control the color space that the interpolation happens in as well as (for "polar" color spaces) control whether hue angle interpolation happens clockwise or counterclockwise. You can read more about this in [Mozilla's blog post about new CSS gradient features](https://developer.mozilla.org/en-US/blog/css-color-module-level-4/).
With this release, esbuild will now automatically transform this syntax for older browsers in the `target` list. For example, here's a gradient that should appear as a rainbow in a browser that supports this new syntax:
```css
/* Original code */
.rainbow-gradient {
width: 100px;
height: 100px;
background: linear-gradient(in hsl longer hue, #7ff, #77f);
}
/* New output (with --target=chrome99) */
.rainbow-gradient {
width: 100px;
height: 100px;
background:
linear-gradient(
#77ffff,
#77ffaa 12.5%,
#77ff80 18.75%,
#84ff77 21.88%,
#99ff77 25%,
#eeff77 37.5%,
#fffb77 40.62%,
#ffe577 43.75%,
#ffbb77 50%,
#ff9077 56.25%,
#ff7b77 59.38%,
#ff7788 62.5%,
#ff77dd 75%,
#ff77f2 78.12%,
#f777ff 81.25%,
#cc77ff 87.5%,
#7777ff);
}
```
You can now use this syntax in your CSS source code and esbuild will automatically convert it to an equivalent gradient for older browsers. In addition, esbuild will now also transform "double position" and "transition hint" syntax for older browsers as appropriate:
```css
/* Original code */
.stripes {
width: 100px;
height: 100px;
background: linear-gradient(#e65 33%, #ff2 33% 67%, #99e 67%);
}
.glow {
width: 100px;
height: 100px;
background: radial-gradient(white 10%, 20%, black);
}
/* New output (with --target=chrome33) */
.stripes {
width: 100px;
height: 100px;
background:
linear-gradient(
#e65 33%,
#ff2 33%,
#ff2 67%,
#99e 67%);
}
.glow {
width: 100px;
height: 100px;
background:
radial-gradient(
#ffffff 10%,
#aaaaaa 12.81%,
#959595 15.62%,
#7b7b7b 21.25%,
#5a5a5a 32.5%,
#444444 43.75%,
#323232 55%,
#161616 77.5%,
#000000);
}
```
If necessary, esbuild will construct a new gradient that approximates the original gradient by recursively splitting the interval in between color stops until the approximation error is within a small threshold. That is why the above output CSS contains many more color stops than the input CSS.
Note that esbuild deliberately _replaces_ the original gradient with the approximation instead of inserting the approximation before the original gradient as a fallback. The latest version of Firefox has multiple gradient rendering bugs (including incorrect interpolation of partially-transparent colors and interpolating non-sRGB colors using the incorrect color space). If esbuild didn't replace the original gradient, then Firefox would use the original gradient instead of the fallback the appearance would be incorrect in Firefox. In other words, the latest version of Firefox supports modern gradient syntax but interprets it incorrectly.
* Add support for `color()`, `lab()`, `lch()`, `oklab()`, `oklch()`, and `hwb()` in CSS
CSS has recently added lots of new ways of specifying colors. This release adds support for lowering and/or minifying colors that use the `color()`, `lab()`, `lch()`, `oklab()`, `oklch()`, or `hwb()` syntax for browsers that don't support it yet:
CSS has recently added lots of new ways of specifying colors. You can read more about this in [Chrome's blog post about CSS color spaces](https://developer.chrome.com/docs/css-ui/high-definition-css-color-guide).
This release adds support for minifying colors that use the `color()`, `lab()`, `lch()`, `oklab()`, `oklch()`, or `hwb()` syntax and/or transforming these colors for browsers that don't support it yet:
```css
/* Original code */
@@ -21,7 +110,7 @@
}
```
As you can see, colors outside of the sRGB color space such as `color(display-p3 1 0 0)` are mapped back into the sRGB gamut and inserted as a fallback for browsers that don't support the new color syntax. You can enable or disable this behavior by setting `--supported:color-functions=` to `true` or `false`.
As you can see, colors outside of the sRGB color space such as `color(display-p3 1 0 0)` are mapped back into the sRGB gamut and inserted as a fallback for browsers that don't support the new color syntax.
* Allow empty type parameter lists in certain cases ([#3512](https://github.com/evanw/esbuild/issues/3512))
+2
View File
@@ -91,6 +91,8 @@ export type CSSFeature = keyof typeof cssFeatures
export const cssFeatures = {
ColorFunctions: true,
GradientDoublePosition: true,
GradientInterpolation: true,
GradientMidpoints: true,
HexRGBA: true,
HWB: true,
InlineStyle: true,
+21 -1
View File
@@ -34,12 +34,32 @@ const cssFeatures: Partial<Record<CSSFeature, string | string[]>> = {
'css.types.color.oklch',
],
GradientDoublePosition: [
'css.types.image.gradient.conic-gradient.doubleposition',
'css.types.image.gradient.linear-gradient.doubleposition',
'css.types.image.gradient.radial-gradient.doubleposition',
'css.types.image.gradient.conic-gradient.doubleposition',
'css.types.image.gradient.repeating-linear-gradient.doubleposition',
'css.types.image.gradient.repeating-radial-gradient.doubleposition',
],
GradientInterpolation: [
'css.types.image.gradient.conic-gradient.hue_interpolation_method',
'css.types.image.gradient.conic-gradient.interpolation_color_space',
'css.types.image.gradient.linear-gradient.hue_interpolation_method',
'css.types.image.gradient.linear-gradient.interpolation_color_space',
'css.types.image.gradient.radial-gradient.hue_interpolation_method',
'css.types.image.gradient.radial-gradient.interpolation_color_space',
'css.types.image.gradient.repeating-conic-gradient.hue_interpolation_method',
'css.types.image.gradient.repeating-conic-gradient.interpolation_color_space',
'css.types.image.gradient.repeating-linear-gradient.hue_interpolation_method',
'css.types.image.gradient.repeating-linear-gradient.interpolation_color_space',
'css.types.image.gradient.repeating-radial-gradient.hue_interpolation_method',
'css.types.image.gradient.repeating-radial-gradient.interpolation_color_space',
],
GradientMidpoints: [
'css.types.image.gradient.linear-gradient.interpolation_hints',
'css.types.image.gradient.radial-gradient.interpolation_hints',
'css.types.image.gradient.repeating-linear-gradient.interpolation_hints',
'css.types.image.gradient.repeating-radial-gradient.interpolation_hints',
],
HexRGBA: 'css.types.color.rgb_hexadecimal_notation.alpha_hexadecimal_notation',
HWB: 'css.types.color.hwb',
InsetProperty: 'css.properties.inset',
+19
View File
@@ -11,6 +11,8 @@ type CSSFeature uint16
const (
ColorFunctions CSSFeature = 1 << iota
GradientDoublePosition
GradientInterpolation
GradientMidpoints
HWB
HexRGBA
InlineStyle
@@ -24,6 +26,8 @@ const (
var StringToCSSFeature = map[string]CSSFeature{
"color-functions": ColorFunctions,
"gradient-double-position": GradientDoublePosition,
"gradient-interpolation": GradientInterpolation,
"gradient-midpoints": GradientMidpoints,
"hwb": HWB,
"hex-rgba": HexRGBA,
"inline-style": InlineStyle,
@@ -59,6 +63,21 @@ var cssTable = map[CSSFeature]map[Engine][]versionRange{
Opera: {{start: v{60, 0, 0}}},
Safari: {{start: v{12, 1, 0}}},
},
GradientInterpolation: {
Chrome: {{start: v{111, 0, 0}}},
Edge: {{start: v{111, 0, 0}}},
IOS: {{start: v{16, 2, 0}}},
Opera: {{start: v{97, 0, 0}}},
Safari: {{start: v{16, 2, 0}}},
},
GradientMidpoints: {
Chrome: {{start: v{40, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
Firefox: {{start: v{36, 0, 0}}},
IOS: {{start: v{7, 0, 0}}},
Opera: {{start: v{27, 0, 0}}},
Safari: {{start: v{7, 0, 0}}},
},
HWB: {
Chrome: {{start: v{101, 0, 0}}},
Edge: {{start: v{101, 0, 0}}},
+277
View File
@@ -4,6 +4,43 @@ import "math"
// Reference: https://drafts.csswg.org/css-color/#color-conversion-code
type colorSpace uint8
const (
colorSpace_a98_rgb colorSpace = iota
colorSpace_display_p3
colorSpace_hsl
colorSpace_hwb
colorSpace_lab
colorSpace_lch
colorSpace_oklab
colorSpace_oklch
colorSpace_prophoto_rgb
colorSpace_rec2020
colorSpace_srgb
colorSpace_srgb_linear
colorSpace_xyz
colorSpace_xyz_d50
colorSpace_xyz_d65
)
func (colorSpace colorSpace) isPolar() bool {
switch colorSpace {
case colorSpace_hsl, colorSpace_hwb, colorSpace_lch, colorSpace_oklch:
return true
}
return false
}
type hueMethod uint8
const (
shorterHue hueMethod = iota
longerHue
increasingHue
decreasingHue
)
func lin_srgb(r float64, g float64, b float64) (float64, float64, float64) {
f := func(val float64) float64 {
if abs := math.Abs(val); abs < 0.04045 {
@@ -48,6 +85,10 @@ func lin_p3(r float64, g float64, b float64) (float64, float64, float64) {
return lin_srgb(r, g, b)
}
func gam_p3(r float64, g float64, b float64) (float64, float64, float64) {
return gam_srgb(r, g, b)
}
func lin_p3_to_xyz(r float64, g float64, b float64) (float64, float64, float64) {
M := [9]float64{
608311.0 / 1250200, 189793.0 / 714400, 198249.0 / 1000160,
@@ -57,6 +98,15 @@ func lin_p3_to_xyz(r float64, g float64, b float64) (float64, float64, float64)
return multiplyMatrices(M, r, g, b)
}
func xyz_to_lin_p3(x float64, y float64, z float64) (float64, float64, float64) {
M := [9]float64{
446124.0 / 178915, -333277.0 / 357830, -72051.0 / 178915,
-14852.0 / 17905, 63121.0 / 35810, 423.0 / 17905,
11844.0 / 330415, -50337.0 / 660830, 316169.0 / 330415,
}
return multiplyMatrices(M, x, y, z)
}
func lin_prophoto(r float64, g float64, b float64) (float64, float64, float64) {
f := func(val float64) float64 {
const Et2 = 16.0 / 512
@@ -69,6 +119,18 @@ func lin_prophoto(r float64, g float64, b float64) (float64, float64, float64) {
return f(r), f(g), f(b)
}
func gam_prophoto(r float64, g float64, b float64) (float64, float64, float64) {
f := func(val float64) float64 {
const Et = 1.0 / 512
if abs := math.Abs(val); abs >= Et {
return math.Copysign(math.Pow(abs, 1/1.8), val)
} else {
return 16 * val
}
}
return f(r), f(g), f(b)
}
func lin_prophoto_to_xyz(r float64, g float64, b float64) (float64, float64, float64) {
M := [9]float64{
0.7977604896723027, 0.13518583717574031, 0.0313493495815248,
@@ -78,6 +140,15 @@ func lin_prophoto_to_xyz(r float64, g float64, b float64) (float64, float64, flo
return multiplyMatrices(M, r, g, b)
}
func xyz_to_lin_prophoto(x float64, y float64, z float64) (float64, float64, float64) {
M := [9]float64{
1.3457989731028281, -0.25558010007997534, -0.05110628506753401,
-0.5446224939028347, 1.5082327413132781, 0.02053603239147973,
0.0, 0.0, 1.2119675456389454,
}
return multiplyMatrices(M, x, y, z)
}
func lin_a98rgb(r float64, g float64, b float64) (float64, float64, float64) {
f := func(val float64) float64 {
return math.Copysign(math.Pow(math.Abs(val), 563.0/256), val)
@@ -85,6 +156,13 @@ func lin_a98rgb(r float64, g float64, b float64) (float64, float64, float64) {
return f(r), f(g), f(b)
}
func gam_a98rgb(r float64, g float64, b float64) (float64, float64, float64) {
f := func(val float64) float64 {
return math.Copysign(math.Pow(math.Abs(val), 256.0/563), val)
}
return f(r), f(g), f(b)
}
func lin_a98rgb_to_xyz(r float64, g float64, b float64) (float64, float64, float64) {
M := [9]float64{
573536.0 / 994567, 263643.0 / 1420810, 187206.0 / 994567,
@@ -94,6 +172,15 @@ func lin_a98rgb_to_xyz(r float64, g float64, b float64) (float64, float64, float
return multiplyMatrices(M, r, g, b)
}
func xyz_to_lin_a98rgb(x float64, y float64, z float64) (float64, float64, float64) {
M := [9]float64{
1829569.0 / 896150, -506331.0 / 896150, -308931.0 / 896150,
-851781.0 / 878810, 1648619.0 / 878810, 36519.0 / 878810,
16779.0 / 1248040, -147721.0 / 1248040, 1266979.0 / 1248040,
}
return multiplyMatrices(M, x, y, z)
}
func lin_2020(r float64, g float64, b float64) (float64, float64, float64) {
f := func(val float64) float64 {
const α = 1.09929682680944
@@ -107,6 +194,19 @@ func lin_2020(r float64, g float64, b float64) (float64, float64, float64) {
return f(r), f(g), f(b)
}
func gam_2020(r float64, g float64, b float64) (float64, float64, float64) {
f := func(val float64) float64 {
const α = 1.09929682680944
const β = 0.018053968510807
if abs := math.Abs(val); abs > β {
return math.Copysign(α*math.Pow(abs, 0.45)-(α-1), val)
} else {
return 4.5 * val
}
}
return f(r), f(g), f(b)
}
func lin_2020_to_xyz(r float64, g float64, b float64) (float64, float64, float64) {
var M = [9]float64{
63426534.0 / 99577255, 20160776.0 / 139408157, 47086771.0 / 278816314,
@@ -116,6 +216,15 @@ func lin_2020_to_xyz(r float64, g float64, b float64) (float64, float64, float64
return multiplyMatrices(M, r, g, b)
}
func xyz_to_lin_2020(x float64, y float64, z float64) (float64, float64, float64) {
M := [9]float64{
30757411.0 / 17917100, -6372589.0 / 17917100, -4539589.0 / 17917100,
-19765991.0 / 29648200, 47925759.0 / 29648200, 467509.0 / 29648200,
792561.0 / 44930125, -1921689.0 / 44930125, 42328811.0 / 44930125,
}
return multiplyMatrices(M, x, y, z)
}
func d65_to_d50(x float64, y float64, z float64) (float64, float64, float64) {
M := [9]float64{
1.0479297925449969, 0.022946870601609652, -0.05019226628920524,
@@ -331,3 +440,171 @@ func gamut_mapping_xyz_to_srgb(x float64, y float64, z float64) (float64, float6
return r, g, b
}
func hsl_to_rgb(hue float64, sat float64, light float64) (float64, float64, float64) {
hue /= 360
hue -= math.Floor(hue)
hue *= 360
sat /= 100
light /= 100
f := func(n float64) float64 {
k := n + hue/30
k /= 12
k -= math.Floor(k)
k *= 12
a := sat * math.Min(light, 1-light)
return light - a*math.Max(-1, math.Min(math.Min(k-3, 9-k), 1))
}
return f(0), f(8), f(4)
}
func rgb_to_hsl(red float64, green float64, blue float64) (float64, float64, float64) {
max := math.Max(math.Max(red, green), blue)
min := math.Min(math.Min(red, green), blue)
hue, sat, light := math.NaN(), 0.0, (min+max)/2
d := max - min
if d != 0 {
if div := math.Min(light, 1-light); div != 0 {
sat = (max - light) / div
}
switch max {
case red:
hue = (green - blue) / d
if green < blue {
hue += 6
}
case green:
hue = (blue-red)/d + 2
case blue:
hue = (red-green)/d + 4
}
hue = hue * 60
}
return hue, sat * 100, light * 100
}
func hwb_to_rgb(hue float64, white float64, black float64) (float64, float64, float64) {
white /= 100
black /= 100
if white+black >= 1 {
gray := white / (white + black)
return gray, gray, gray
}
r, g, b := hsl_to_rgb(hue, 100, 50)
r = white + r*(1-white-black)
g = white + g*(1-white-black)
b = white + b*(1-white-black)
return r, g, b
}
func rgb_to_hwb(red float64, green float64, blue float64) (float64, float64, float64) {
h, _, _ := rgb_to_hsl(red, green, blue)
white := math.Min(math.Min(red, green), blue)
black := 1 - math.Max(math.Max(red, green), blue)
return h, white * 100, black * 100
}
func xyz_to_colorSpace(x float64, y float64, z float64, colorSpace colorSpace) (float64, float64, float64) {
switch colorSpace {
case colorSpace_a98_rgb:
return gam_a98rgb(xyz_to_lin_a98rgb(x, y, z))
case colorSpace_display_p3:
return gam_p3(xyz_to_lin_p3(x, y, z))
case colorSpace_hsl:
return rgb_to_hsl(gam_srgb(xyz_to_lin_srgb(x, y, z)))
case colorSpace_hwb:
return rgb_to_hwb(gam_srgb(xyz_to_lin_srgb(x, y, z)))
case colorSpace_lab:
return xyz_to_lab(d65_to_d50(x, y, z))
case colorSpace_lch:
return lab_to_lch(xyz_to_lab(d65_to_d50(x, y, z)))
case colorSpace_oklab:
return xyz_to_oklab(x, y, z)
case colorSpace_oklch:
return oklab_to_oklch(xyz_to_oklab(x, y, z))
case colorSpace_prophoto_rgb:
return gam_prophoto(xyz_to_lin_prophoto(d65_to_d50(x, y, z)))
case colorSpace_rec2020:
return gam_2020(xyz_to_lin_2020(x, y, z))
case colorSpace_srgb:
return gam_srgb(xyz_to_lin_srgb(x, y, z))
case colorSpace_srgb_linear:
return xyz_to_lin_srgb(x, y, z)
case colorSpace_xyz, colorSpace_xyz_d65:
return x, y, z
case colorSpace_xyz_d50:
return d65_to_d50(x, y, z)
default:
panic("Internal error")
}
}
func colorSpace_to_xyz(v0 float64, v1 float64, v2 float64, colorSpace colorSpace) (float64, float64, float64) {
switch colorSpace {
case colorSpace_a98_rgb:
return lin_a98rgb_to_xyz(lin_a98rgb(v0, v1, v2))
case colorSpace_display_p3:
return lin_p3_to_xyz(lin_p3(v0, v1, v2))
case colorSpace_hsl:
return lin_srgb_to_xyz(lin_srgb(hsl_to_rgb(v0, v1, v2)))
case colorSpace_hwb:
return lin_srgb_to_xyz(lin_srgb(hwb_to_rgb(v0, v1, v2)))
case colorSpace_lab:
return d50_to_d65(lab_to_xyz(v0, v1, v2))
case colorSpace_lch:
return d50_to_d65(lab_to_xyz(lch_to_lab(v0, v1, v2)))
case colorSpace_oklab:
return oklab_to_xyz(v0, v1, v2)
case colorSpace_oklch:
return oklab_to_xyz(oklch_to_oklab(v0, v1, v2))
case colorSpace_prophoto_rgb:
return d50_to_d65(lin_prophoto_to_xyz(lin_prophoto(v0, v1, v2)))
case colorSpace_rec2020:
return lin_2020_to_xyz(lin_2020(v0, v1, v2))
case colorSpace_srgb:
return lin_srgb_to_xyz(lin_srgb(v0, v1, v2))
case colorSpace_srgb_linear:
return lin_srgb_to_xyz(v0, v1, v2)
case colorSpace_xyz, colorSpace_xyz_d65:
return v0, v1, v2
case colorSpace_xyz_d50:
return d50_to_d65(v0, v1, v2)
default:
panic("Internal error")
}
}
+41 -38
View File
@@ -287,13 +287,13 @@ func (p *parser) lowerAndMinifyColor(token css_ast.Token, wouldClipColor *bool)
// "#1234" => "rgba(1, 2, 3, 0.004)"
if hex, ok := parseHex(text); ok {
hex = expandHex(hex)
return p.tryToGenerateColor(token, parsedColor{sRGB: true, hex: hex}, nil)
return p.tryToGenerateColor(token, parsedColor{hex: hex}, nil)
}
case 8:
// "#12345678" => "rgba(18, 52, 86, 0.47)"
if hex, ok := parseHex(text); ok {
return p.tryToGenerateColor(token, parsedColor{sRGB: true, hex: hex}, nil)
return p.tryToGenerateColor(token, parsedColor{hex: hex}, nil)
}
}
}
@@ -420,9 +420,9 @@ func (p *parser) lowerAndMinifyColor(token css_ast.Token, wouldClipColor *bool)
}
type parsedColor struct {
x, y, z float64 // color if sRGB == false
hex uint32 // color and alpha if sRGB == true, alpha if sRGB == false
sRGB bool
x, y, z float64 // color if hasColorSpace == true
hex uint32 // color and alpha if hasColorSpace == false, alpha if hasColorSpace == true
hasColorSpace bool
}
func looksLikeColor(token css_ast.Token) bool {
@@ -467,7 +467,7 @@ func parseColor(token css_ast.Token) (parsedColor, bool) {
switch token.Kind {
case css_lexer.TIdent:
if hex, ok := colorNameToHex[strings.ToLower(text)]; ok {
return parsedColor{sRGB: true, hex: hex}, true
return parsedColor{hex: hex}, true
}
case css_lexer.THash:
@@ -475,25 +475,25 @@ func parseColor(token css_ast.Token) (parsedColor, bool) {
case 3:
// "#123"
if hex, ok := parseHex(text); ok {
return parsedColor{sRGB: true, hex: (expandHex(hex) << 8) | 0xFF}, true
return parsedColor{hex: (expandHex(hex) << 8) | 0xFF}, true
}
case 4:
// "#1234"
if hex, ok := parseHex(text); ok {
return parsedColor{sRGB: true, hex: expandHex(hex)}, true
return parsedColor{hex: expandHex(hex)}, true
}
case 6:
// "#112233"
if hex, ok := parseHex(text); ok {
return parsedColor{sRGB: true, hex: (hex << 8) | 0xFF}, true
return parsedColor{hex: (hex << 8) | 0xFF}, true
}
case 8:
// "#11223344"
if hex, ok := parseHex(text); ok {
return parsedColor{sRGB: true, hex: hex}, true
return parsedColor{hex: hex}, true
}
}
@@ -532,7 +532,7 @@ func parseColor(token css_ast.Token) (parsedColor, bool) {
if g, ok := parseColorByte(g, 1); ok {
if b, ok := parseColorByte(b, 1); ok {
if a, ok := parseAlphaByte(a); ok {
return parsedColor{sRGB: true, hex: (r << 24) | (g << 16) | (b << 8) | a}, true
return parsedColor{hex: (r << 24) | (g << 16) | (b << 8) | a}, true
}
}
}
@@ -572,7 +572,7 @@ func parseColor(token css_ast.Token) (parsedColor, bool) {
if l, ok := l.ClampedFractionForPercentage(); ok {
if a, ok := parseAlphaByte(a); ok {
r, g, b := hslToRgb(h, s, l)
return parsedColor{sRGB: true, hex: packRGBA(r, g, b, a)}, true
return parsedColor{hex: packRGBA(r, g, b, a)}, true
}
}
}
@@ -600,7 +600,7 @@ func parseColor(token css_ast.Token) (parsedColor, bool) {
if black, ok := l.ClampedFractionForPercentage(); ok {
if a, ok := parseAlphaByte(a); ok {
r, g, b := hwbToRgb(h, white, black)
return parsedColor{sRGB: true, hex: packRGBA(r, g, b, a)}, true
return parsedColor{hex: packRGBA(r, g, b, a)}, true
}
}
}
@@ -631,42 +631,39 @@ func parseColor(token css_ast.Token) (parsedColor, bool) {
case "a98-rgb":
r, g, b := lin_a98rgb(v0, v1, v2)
x, y, z := lin_a98rgb_to_xyz(r, g, b)
return parsedColor{x: x, y: y, z: z, hex: a}, true
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
case "display-p3":
r, g, b := lin_p3(v0, v1, v2)
x, y, z := lin_p3_to_xyz(r, g, b)
return parsedColor{x: x, y: y, z: z, hex: a}, true
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
case "prophoto-rgb":
r, g, b := lin_prophoto(v0, v1, v2)
x, y, z := lin_prophoto_to_xyz(r, g, b)
x, y, z = d50_to_d65(x, y, z)
return parsedColor{x: x, y: y, z: z, hex: a}, true
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
case "rec2020":
r, g, b := lin_2020(v0, v1, v2)
x, y, z := lin_2020_to_xyz(r, g, b)
return parsedColor{x: x, y: y, z: z, hex: a}, true
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
case "srgb":
r, g, b := lin_srgb(v0, v1, v2)
x, y, z := lin_srgb_to_xyz(r, g, b)
return parsedColor{x: x, y: y, z: z, hex: a}, true
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
case "srgb-linear":
x, y, z := lin_srgb_to_xyz(v0, v1, v2)
return parsedColor{x: x, y: y, z: z, hex: a}, true
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
case "xyz":
return parsedColor{x: v0, y: v1, z: v2, hex: a}, true
case "xyz", "xyz-d65":
return parsedColor{hasColorSpace: true, x: v0, y: v1, z: v2, hex: a}, true
case "xyz-d50":
x, y, z := d50_to_d65(v0, v1, v2)
return parsedColor{x: x, y: y, z: z, hex: a}, true
case "xyz-d65":
return parsedColor{x: v0, y: v1, z: v2, hex: a}, true
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
}
}
}
@@ -699,7 +696,7 @@ func parseColor(token css_ast.Token) (parsedColor, bool) {
if v2, ok := v2.NumberOrFractionForPercentage(125, css_ast.AllowAnyPercentage); ok {
x, y, z := lab_to_xyz(v0, v1, v2)
x, y, z = d50_to_d65(x, y, z)
return parsedColor{x: x, y: y, z: z, hex: alpha}, true
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: alpha}, true
}
}
}
@@ -711,7 +708,7 @@ func parseColor(token css_ast.Token) (parsedColor, bool) {
l, a, b := lch_to_lab(v0, v1, v2)
x, y, z := lab_to_xyz(l, a, b)
x, y, z = d50_to_d65(x, y, z)
return parsedColor{x: x, y: y, z: z, hex: alpha}, true
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: alpha}, true
}
}
}
@@ -721,7 +718,7 @@ func parseColor(token css_ast.Token) (parsedColor, bool) {
if v1, ok := v1.NumberOrFractionForPercentage(0.4, css_ast.AllowAnyPercentage); ok {
if v2, ok := v2.NumberOrFractionForPercentage(0.4, css_ast.AllowAnyPercentage); ok {
x, y, z := oklab_to_xyz(v0, v1, v2)
return parsedColor{x: x, y: y, z: z, hex: alpha}, true
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: alpha}, true
}
}
}
@@ -732,7 +729,7 @@ func parseColor(token css_ast.Token) (parsedColor, bool) {
if v2, ok := degreesForAngle(v2); ok {
l, a, b := oklch_to_oklab(v0, v1, v2)
x, y, z := oklab_to_xyz(l, a, b)
return parsedColor{x: x, y: y, z: z, hex: alpha}, true
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: alpha}, true
}
}
}
@@ -841,6 +838,14 @@ func parseColorByte(token css_ast.Token, scale float64) (uint32, bool) {
return uint32(i), ok
}
func tryToConvertToHexWithoutClipping(x float64, y float64, z float64, a uint32) (uint32, bool) {
r, g, b := gam_srgb(xyz_to_lin_srgb(x, y, z))
if r < -0.5/255 || r > 255.5/255 || g < -0.5/255 || g > 255.5/255 || b < -0.5/255 || b > 255.5/255 {
return 0, false
}
return packRGBA(r, g, b, a), true
}
func (p *parser) tryToGenerateColor(token css_ast.Token, color parsedColor, wouldClipColor *bool) css_ast.Token {
// Note: Do NOT remove color information from fully transparent colors.
// Safari behaves differently than other browsers for color interpolation:
@@ -849,17 +854,15 @@ func (p *parser) tryToGenerateColor(token css_ast.Token, color parsedColor, woul
// Attempt to convert other color spaces to sRGB, and only continue if the
// result (rounded to the nearest byte) will be in the 0-to-1 sRGB range
var hex uint32
if color.sRGB {
if !color.hasColorSpace {
hex = color.hex
} else if result, ok := tryToConvertToHexWithoutClipping(color.x, color.y, color.z, color.hex); ok {
hex = result
} else if wouldClipColor != nil {
*wouldClipColor = true
return token
} else {
r, g, b := gam_srgb(xyz_to_lin_srgb(color.x, color.y, color.z))
if r < -0.5/255 || r > 255.5/255 || g < -0.5/255 || g > 255.5/255 || b < -0.5/255 || b > 255.5/255 {
if wouldClipColor != nil {
*wouldClipColor = true
return token
}
r, g, b = gamut_mapping_xyz_to_srgb(color.x, color.y, color.z)
}
r, g, b := gamut_mapping_xyz_to_srgb(color.x, color.y, color.z)
hex = packRGBA(r, g, b, color.hex)
}
+774 -14
View File
@@ -1,11 +1,15 @@
package css_parser
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/logger"
)
type gradientKind uint8
@@ -17,7 +21,7 @@ const (
)
type parsedGradient struct {
initialTokens []css_ast.Token
leadingTokens []css_ast.Token
colorStops []colorStop
kind gradientKind
repeating bool
@@ -26,7 +30,7 @@ type parsedGradient struct {
type colorStop struct {
positions []css_ast.Token
color css_ast.Token
hint css_ast.Token // Absent if "hint.Kind == css_lexer.T(0)"
midpoint css_ast.Token // Absent if "midpoint.Kind == css_lexer.T(0)"
}
func parseGradient(token css_ast.Token) (gradient parsedGradient, success bool) {
@@ -74,7 +78,7 @@ func parseGradient(token css_ast.Token) (gradient parsedGradient, success bool)
for i < len(tokens) && tokens[i].Kind != css_lexer.TComma {
i++
}
gradient.initialTokens = tokens[:i]
gradient.leadingTokens = tokens[:i]
if i < len(tokens) {
tokens = tokens[i+1:]
} else {
@@ -104,7 +108,7 @@ func parseGradient(token css_ast.Token) (gradient parsedGradient, success bool)
}
// Parse the comma
var hint css_ast.Token
var midpoint css_ast.Token
if len(tokens) > 0 {
if tokens[0].Kind != css_lexer.TComma {
return
@@ -114,9 +118,9 @@ func parseGradient(token css_ast.Token) (gradient parsedGradient, success bool)
return
}
// Parse the hint, if any
// Parse the midpoint, if any
if len(tokens) > 0 && tokens[0].Kind.IsNumeric() {
hint = tokens[0]
midpoint = tokens[0]
tokens = tokens[1:]
// Followed by a mandatory comma
@@ -131,7 +135,7 @@ func parseGradient(token css_ast.Token) (gradient parsedGradient, success bool)
gradient.colorStops = append(gradient.colorStops, colorStop{
color: color,
positions: positions,
hint: hint,
midpoint: midpoint,
})
}
@@ -143,15 +147,15 @@ func (p *parser) generateGradient(token css_ast.Token, gradient parsedGradient)
var children []css_ast.Token
commaToken := p.commaToken(token.Loc)
children = append(children, gradient.initialTokens...)
children = append(children, gradient.leadingTokens...)
for _, stop := range gradient.colorStops {
if len(children) > 0 {
children = append(children, commaToken)
}
children = append(children, stop.color)
children = append(children, stop.positions...)
if stop.hint.Kind != css_lexer.T(0) {
children = append(children, commaToken, stop.hint)
if stop.midpoint.Kind != css_lexer.T(0) {
children = append(children, commaToken, stop.midpoint)
}
}
@@ -165,6 +169,54 @@ func (p *parser) lowerAndMinifyGradient(token css_ast.Token, wouldClipColor *boo
return token
}
lowerMidpoints := p.options.unsupportedCSSFeatures.Has(compat.GradientMidpoints)
lowerColorSpaces := p.options.unsupportedCSSFeatures.Has(compat.ColorFunctions)
lowerInterpolation := p.options.unsupportedCSSFeatures.Has(compat.GradientInterpolation)
// Assume that if the browser doesn't support color spaces in gradients, then
// it doesn't correctly interpolate non-sRGB colors even when a color space
// is not specified. This is the case for Firefox 120, for example, which has
// support for the "color()" syntax but not for color spaces in gradients.
// There is no entry in our feature support matrix for this edge case so we
// make this assumption instead.
//
// Note that this edge case means we have to _replace_ the original gradient
// with the expanded one instead of inserting a fallback before it. Otherwise
// Firefox 120 would use the original gradient instead of the fallback because
// it supports the syntax, but just renders it incorrectly.
if lowerInterpolation {
lowerColorSpaces = true
}
// Potentially expand the gradient to handle unsupported features
if lowerMidpoints || lowerColorSpaces || lowerInterpolation {
if colorStops, ok := tryToParseColorStops(gradient); ok {
hasColorSpace := false
hasMidpoint := false
for _, stop := range colorStops {
if stop.hasColorSpace {
hasColorSpace = true
}
if stop.midpoint != nil {
hasMidpoint = true
}
}
remaining, colorSpace, hueMethod, hasInterpolation := removeColorInterpolation(gradient.leadingTokens)
if (hasInterpolation && lowerInterpolation) || (hasColorSpace && lowerColorSpaces) || (hasMidpoint && lowerMidpoints) {
if hasInterpolation {
tryToExpandGradient(token.Loc, &gradient, colorStops, remaining, colorSpace, hueMethod)
} else {
if hasColorSpace {
colorSpace = colorSpace_oklab
} else {
colorSpace = colorSpace_srgb
}
tryToExpandGradient(token.Loc, &gradient, colorStops, gradient.leadingTokens, colorSpace, shorterHue)
}
}
}
}
// Lower all colors in the gradient stop
for i, stop := range gradient.colorStops {
gradient.colorStops[i].color = p.lowerAndMinifyColor(stop.color, wouldClipColor)
@@ -182,7 +234,7 @@ func (p *parser) lowerAndMinifyGradient(token css_ast.Token, wouldClipColor *boo
// Replace duplicated single positions with double positions
for i, stop := range gradient.colorStops {
if i > 0 && len(stop.positions) == 1 {
if prev := gradient.colorStops[i-1]; len(prev.positions) == 1 && prev.hint.Kind == css_lexer.T(0) &&
if prev := gradient.colorStops[i-1]; len(prev.positions) == 1 && prev.midpoint.Kind == css_lexer.T(0) &&
css_ast.TokensEqual([]css_ast.Token{prev.color}, []css_ast.Token{stop.color}, nil) {
gradient.colorStops = switchToDoublePositions(gradient.colorStops)
break
@@ -202,7 +254,7 @@ func switchToSinglePositions(double []colorStop) (single []colorStop) {
for len(stop.positions) > 1 {
clone := stop
clone.positions = stop.positions[:1]
clone.hint = css_ast.Token{}
clone.midpoint = css_ast.Token{}
single = append(single, clone)
stop.positions = stop.positions[1:]
}
@@ -214,13 +266,13 @@ func switchToSinglePositions(double []colorStop) (single []colorStop) {
func switchToDoublePositions(single []colorStop) (double []colorStop) {
for i := 0; i < len(single); i++ {
stop := single[i]
if i+1 < len(single) && len(stop.positions) == 1 && stop.hint.Kind == css_lexer.T(0) {
if i+1 < len(single) && len(stop.positions) == 1 && stop.midpoint.Kind == css_lexer.T(0) {
if next := single[i+1]; len(next.positions) == 1 &&
css_ast.TokensEqual([]css_ast.Token{stop.color}, []css_ast.Token{next.color}, nil) {
double = append(double, colorStop{
color: stop.color,
positions: []css_ast.Token{stop.positions[0], next.positions[0]},
hint: next.hint,
midpoint: next.midpoint,
})
i++
continue
@@ -230,3 +282,711 @@ func switchToDoublePositions(single []colorStop) (double []colorStop) {
}
return
}
func removeColorInterpolation(tokens []css_ast.Token) ([]css_ast.Token, colorSpace, hueMethod, bool) {
for i := 0; i+1 < len(tokens); i++ {
if in := tokens[i]; in.Kind == css_lexer.TIdent && strings.EqualFold(in.Text, "in") {
if space := tokens[i+1]; space.Kind == css_lexer.TIdent {
var colorSpace colorSpace
hueMethod := shorterHue
start := i
end := i + 2
// Parse the color space
switch strings.ToLower(space.Text) {
case "a98-rgb":
colorSpace = colorSpace_a98_rgb
case "display-p3":
colorSpace = colorSpace_display_p3
case "hsl":
colorSpace = colorSpace_hsl
case "hwb":
colorSpace = colorSpace_hwb
case "lab":
colorSpace = colorSpace_lab
case "lch":
colorSpace = colorSpace_lch
case "oklab":
colorSpace = colorSpace_oklab
case "oklch":
colorSpace = colorSpace_oklch
case "prophoto-rgb":
colorSpace = colorSpace_prophoto_rgb
case "rec2020":
colorSpace = colorSpace_rec2020
case "srgb":
colorSpace = colorSpace_srgb
case "srgb-linear":
colorSpace = colorSpace_srgb_linear
case "xyz":
colorSpace = colorSpace_xyz
case "xyz-d50":
colorSpace = colorSpace_xyz_d50
case "xyz-d65":
colorSpace = colorSpace_xyz_d65
default:
return nil, 0, 0, false
}
// Parse the optional hue mode for polar color spaces
if colorSpace.isPolar() && i+3 < len(tokens) {
if hue := tokens[i+3]; hue.Kind == css_lexer.TIdent && strings.EqualFold(hue.Text, "hue") {
if method := tokens[i+2]; method.Kind == css_lexer.TIdent {
switch strings.ToLower(method.Text) {
case "shorter":
hueMethod = shorterHue
case "longer":
hueMethod = longerHue
case "increasing":
hueMethod = increasingHue
case "decreasing":
hueMethod = decreasingHue
default:
return nil, 0, 0, false
}
end = i + 4
}
}
}
// Remove all parsed tokens
remaining := append(append([]css_ast.Token{}, tokens[:start]...), tokens[end:]...)
if n := len(remaining); n > 0 {
remaining[0].Whitespace &= ^css_ast.WhitespaceBefore
remaining[n-1].Whitespace &= ^css_ast.WhitespaceAfter
}
return remaining, colorSpace, hueMethod, true
}
}
}
return nil, 0, 0, false
}
type valueWithUnit struct {
unit string
value float64
}
type parsedColorStop struct {
// Position information (may be a sum of two different units)
positionTerms []valueWithUnit
// Color midpoint (a.k.a. transition hint) information
midpoint *valueWithUnit
// Non-premultiplied color information in XYZ space
x, y, z, alpha float64
// Non-premultiplied color information in sRGB space
r, g, b float64
// Premultiplied color information in the interpolation color space
v0, v1, v2 float64
// True if the original color has a color space
hasColorSpace bool
}
func tryToParseColorStops(gradient parsedGradient) ([]parsedColorStop, bool) {
var colorStops []parsedColorStop
for _, stop := range gradient.colorStops {
color, ok := parseColor(stop.color)
if !ok {
return nil, false
}
var r, g, b float64
if !color.hasColorSpace {
r = float64(hexR(color.hex)) / 255
g = float64(hexG(color.hex)) / 255
b = float64(hexB(color.hex)) / 255
color.x, color.y, color.z = lin_srgb_to_xyz(lin_srgb(r, g, b))
} else {
r, g, b = gam_srgb(xyz_to_lin_srgb(color.x, color.y, color.z))
}
parsedStop := parsedColorStop{
x: color.x,
y: color.y,
z: color.z,
r: r,
g: g,
b: b,
alpha: float64(hexA(color.hex)) / 255,
hasColorSpace: color.hasColorSpace,
}
for i, position := range stop.positions {
if position, ok := tryToParseValue(position, gradient.kind); ok {
parsedStop.positionTerms = []valueWithUnit{position}
} else {
return nil, false
}
// Expand double positions
if i+1 < len(stop.positions) {
colorStops = append(colorStops, parsedStop)
}
}
if stop.midpoint.Kind != css_lexer.T(0) {
if midpoint, ok := tryToParseValue(stop.midpoint, gradient.kind); ok {
parsedStop.midpoint = &midpoint
} else {
return nil, false
}
}
colorStops = append(colorStops, parsedStop)
}
// Automatically fill in missing positions
if len(colorStops) > 0 {
type stopInfo struct {
fromPos valueWithUnit
toPos valueWithUnit
fromCount int32
toCount int32
}
// Fill in missing positions for the endpoints first
if first := &colorStops[0]; len(first.positionTerms) == 0 {
first.positionTerms = []valueWithUnit{{value: 0, unit: "%"}}
}
if last := &colorStops[len(colorStops)-1]; len(last.positionTerms) == 0 {
last.positionTerms = []valueWithUnit{{value: 100, unit: "%"}}
}
// Set all positions to be greater than the position before them
for i, stop := range colorStops {
var prevPos valueWithUnit
for j := i - 1; j >= 0; j-- {
prev := colorStops[j]
if prev.midpoint != nil {
prevPos = *prev.midpoint
break
}
if len(prev.positionTerms) == 1 {
prevPos = prev.positionTerms[0]
break
}
}
if len(stop.positionTerms) == 1 {
if prevPos.unit == stop.positionTerms[0].unit {
stop.positionTerms[0].value = math.Max(prevPos.value, stop.positionTerms[0].value)
}
prevPos = stop.positionTerms[0]
}
if stop.midpoint != nil && prevPos.unit == stop.midpoint.unit {
stop.midpoint.value = math.Max(prevPos.value, stop.midpoint.value)
}
}
// Scan over all other stops with missing positions
infos := make([]stopInfo, len(colorStops))
for i, stop := range colorStops {
if len(stop.positionTerms) == 1 {
continue
}
info := &infos[i]
// Scan backward
for from := i - 1; from >= 0; from-- {
fromStop := colorStops[from]
info.fromCount++
if fromStop.midpoint != nil {
info.fromPos = *fromStop.midpoint
break
}
if len(fromStop.positionTerms) == 1 {
info.fromPos = fromStop.positionTerms[0]
break
}
}
// Scan forward
for to := i; to < len(colorStops); to++ {
info.toCount++
if toStop := colorStops[to]; toStop.midpoint != nil {
info.toPos = *toStop.midpoint
break
}
if to+1 < len(colorStops) {
if toStop := colorStops[to+1]; len(toStop.positionTerms) == 1 {
info.toPos = toStop.positionTerms[0]
break
}
}
}
}
// Then fill in all other missing positions
for i, stop := range colorStops {
if len(stop.positionTerms) != 1 {
info := infos[i]
t := float64(info.fromCount) / float64(info.fromCount+info.toCount)
if info.fromPos.unit == info.toPos.unit {
colorStops[i].positionTerms = []valueWithUnit{{
value: info.fromPos.value + (info.toPos.value-info.fromPos.value)*t,
unit: info.fromPos.unit,
}}
} else {
colorStops[i].positionTerms = []valueWithUnit{{
value: info.fromPos.value * (1 - t),
unit: info.fromPos.unit,
}, {
value: info.toPos.value * t,
unit: info.toPos.unit,
}}
}
}
}
// Midpoints are only supported if they use the same units as their neighbors
for i, stop := range colorStops {
if stop.midpoint != nil {
next := colorStops[i+1]
if len(stop.positionTerms) != 1 || stop.midpoint.unit != stop.positionTerms[0].unit ||
len(next.positionTerms) != 1 || stop.midpoint.unit != next.positionTerms[0].unit {
return nil, false
}
}
}
}
return colorStops, true
}
func tryToParseValue(token css_ast.Token, kind gradientKind) (result valueWithUnit, success bool) {
if kind == conicGradient {
// <angle-percentage>
switch token.Kind {
case css_lexer.TDimension:
degrees, ok := degreesForAngle(token)
if !ok {
return
}
result.value = degrees * (100.0 / 360)
result.unit = "%"
case css_lexer.TPercentage:
percent, err := strconv.ParseFloat(token.PercentageValue(), 64)
if err != nil {
return
}
result.value = percent
result.unit = "%"
default:
return
}
} else {
// <length-percentage>
switch token.Kind {
case css_lexer.TNumber:
zero, err := strconv.ParseFloat(token.Text, 64)
if err != nil || zero != 0 {
return
}
result.value = 0
result.unit = "%"
case css_lexer.TDimension:
dimensionValue, err := strconv.ParseFloat(token.DimensionValue(), 64)
if err != nil {
return
}
result.value = dimensionValue
result.unit = token.DimensionUnit()
case css_lexer.TPercentage:
percentageValue, err := strconv.ParseFloat(token.PercentageValue(), 64)
if err != nil {
return
}
result.value = percentageValue
result.unit = "%"
default:
return
}
}
success = true
return
}
func tryToExpandGradient(
loc logger.Loc,
gradient *parsedGradient,
colorStops []parsedColorStop,
remaining []css_ast.Token,
colorSpace colorSpace,
hueMethod hueMethod,
) bool {
// Convert color stops into the interpolation color space
for i := range colorStops {
stop := &colorStops[i]
v0, v1, v2 := xyz_to_colorSpace(stop.x, stop.y, stop.z, colorSpace)
stop.v0, stop.v1, stop.v2 = premultiply(v0, v1, v2, stop.alpha, colorSpace)
}
// Duplicate the endpoints if they should wrap around to themselves
if hueMethod == longerHue && colorSpace.isPolar() && len(colorStops) > 0 {
if first := colorStops[0]; len(first.positionTerms) == 1 {
if first.positionTerms[0].value < 0 {
colorStops[0].positionTerms[0].value = 0
} else if first.positionTerms[0].value > 0 {
first.midpoint = nil
first.positionTerms = []valueWithUnit{{value: 0, unit: first.positionTerms[0].unit}}
colorStops = append([]parsedColorStop{first}, colorStops...)
}
}
if last := colorStops[len(colorStops)-1]; len(last.positionTerms) == 1 {
if last.positionTerms[0].unit != "%" || last.positionTerms[0].value < 100 {
last.positionTerms = []valueWithUnit{{value: 100, unit: "%"}}
colorStops = append(colorStops, last)
}
}
}
var newColorStops []colorStop
var generateColorStops func(
int, parsedColorStop, parsedColorStop,
float64, float64, float64, float64, float64, float64, float64, float64,
float64, float64, float64, float64, float64, float64, float64, float64,
)
generateColorStops = func(
depth int,
from parsedColorStop, to parsedColorStop,
prevX, prevY, prevZ, prevR, prevG, prevB, prevA, prevT float64,
nextX, nextY, nextZ, nextR, nextG, nextB, nextA, nextT float64,
) {
if depth > 4 {
return
}
t := (prevT + nextT) / 2
positionT := t
// Handle midpoints (which we have already checked uses the same units)
if from.midpoint != nil {
fromPos := from.positionTerms[0].value
toPos := to.positionTerms[0].value
stopPos := fromPos + (toPos-fromPos)*t
H := (from.midpoint.value - fromPos) / (toPos - fromPos)
P := (stopPos - fromPos) / (toPos - fromPos)
if H <= 0 {
positionT = 1
} else if H >= 1 {
positionT = 0
} else {
positionT = math.Pow(P, -1/math.Log2(H))
}
}
v0, v1, v2 := interpolateColors(from.v0, from.v1, from.v2, to.v0, to.v1, to.v2, colorSpace, hueMethod, positionT)
a := from.alpha + (to.alpha-from.alpha)*positionT
v0, v1, v2 = unpremultiply(v0, v1, v2, a, colorSpace)
x, y, z := colorSpace_to_xyz(v0, v1, v2, colorSpace)
// Stop when the color is similar enough to the sRGB midpoint
const epsilon = 4.0 / 255
r, g, b := gam_srgb(xyz_to_lin_srgb(x, y, z))
dr := r*a - (prevR*prevA+nextR*nextA)/2
dg := g*a - (prevG*prevA+nextG*nextA)/2
db := b*a - (prevB*prevA+nextB*nextA)/2
if d := dr*dr + dg*dg + db*db; d < epsilon*epsilon {
return
}
// Recursive split before this stop
generateColorStops(depth+1, from, to,
prevX, prevY, prevZ, prevR, prevG, prevB, prevA, prevT,
x, y, z, r, g, b, a, t)
// Generate this stop
color := makeColorToken(loc, x, y, z, a)
positionTerms := interpolatePositions(from.positionTerms, to.positionTerms, t)
position := makePositionToken(loc, positionTerms)
position.Whitespace = css_ast.WhitespaceBefore
newColorStops = append(newColorStops, colorStop{
color: color,
positions: []css_ast.Token{position},
})
// Recursive split after this stop
generateColorStops(depth+1, from, to,
x, y, z, r, g, b, a, t,
nextX, nextY, nextZ, nextR, nextG, nextB, nextA, nextT)
}
for i, stop := range colorStops {
color := makeColorToken(loc, stop.x, stop.y, stop.z, stop.alpha)
position := makePositionToken(loc, stop.positionTerms)
position.Whitespace = css_ast.WhitespaceBefore
newColorStops = append(newColorStops, colorStop{
color: color,
positions: []css_ast.Token{position},
})
// Generate new color stops in between as needed
if i+1 < len(colorStops) {
next := colorStops[i+1]
generateColorStops(0, stop, next,
stop.x, stop.y, stop.z, stop.r, stop.g, stop.b, stop.alpha, 0,
next.x, next.y, next.z, next.r, next.g, next.b, next.alpha, 1)
}
}
// Remove implied positions for neatness
if len(newColorStops) > 0 {
first := newColorStops[0].positions[0]
if (first.Kind == css_lexer.TPercentage && first.PercentageValue() == "0") ||
(first.Kind == css_lexer.TDimension && first.DimensionValue() == "0") {
newColorStops[0].positions = nil
}
last := newColorStops[len(newColorStops)-1].positions[0]
if last.Kind == css_lexer.TPercentage && last.PercentageValue() == "100" {
newColorStops[len(newColorStops)-1].positions = nil
}
}
gradient.leadingTokens = remaining
gradient.colorStops = newColorStops
return true
}
func formatFloat(value float64, decimals int) string {
return strings.TrimSuffix(strings.TrimRight(strconv.FormatFloat(value, 'f', decimals, 64), "0"), ".")
}
func makeDimensionOrPercentToken(loc logger.Loc, value float64, unit string) (token css_ast.Token) {
token.Loc = loc
token.Text = formatFloat(value, 2)
if unit == "%" {
token.Kind = css_lexer.TPercentage
} else {
token.Kind = css_lexer.TDimension
token.UnitOffset = uint16(len(token.Text))
}
token.Text += unit
return
}
func makePositionToken(loc logger.Loc, positionTerms []valueWithUnit) css_ast.Token {
if len(positionTerms) == 1 {
return makeDimensionOrPercentToken(loc, positionTerms[0].value, positionTerms[0].unit)
}
children := make([]css_ast.Token, 0, 1+2*len(positionTerms))
for i, term := range positionTerms {
if i > 0 {
children = append(children, css_ast.Token{
Loc: loc,
Kind: css_lexer.TDelimPlus,
Text: "+",
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
})
}
children = append(children, makeDimensionOrPercentToken(loc, term.value, term.unit))
}
return css_ast.Token{
Loc: loc,
Kind: css_lexer.TFunction,
Text: "calc",
Children: &children,
}
}
func makeColorToken(loc logger.Loc, x float64, y float64, z float64, a float64) (color css_ast.Token) {
color.Loc = loc
alpha := uint32(math.Round(a * 255))
if hex, ok := tryToConvertToHexWithoutClipping(x, y, z, alpha); ok {
color.Kind = css_lexer.THash
if alpha == 255 {
color.Text = fmt.Sprintf("%06x", hex>>8)
} else {
color.Text = fmt.Sprintf("%08x", hex)
}
} else {
children := []css_ast.Token{
{
Loc: loc,
Kind: css_lexer.TIdent,
Text: "xyz",
Whitespace: css_ast.WhitespaceAfter,
},
{
Loc: loc,
Kind: css_lexer.TNumber,
Text: formatFloat(x, 3),
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
},
{
Loc: loc,
Kind: css_lexer.TNumber,
Text: formatFloat(y, 3),
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
},
{
Loc: loc,
Kind: css_lexer.TNumber,
Text: formatFloat(z, 3),
Whitespace: css_ast.WhitespaceBefore,
},
}
if a < 1 {
children = append(children,
css_ast.Token{
Loc: loc,
Kind: css_lexer.TDelimSlash,
Text: "/",
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
},
css_ast.Token{
Loc: loc,
Kind: css_lexer.TNumber,
Text: formatFloat(a, 3),
Whitespace: css_ast.WhitespaceBefore,
},
)
}
color.Kind = css_lexer.TFunction
color.Text = "color"
color.Children = &children
}
return
}
func interpolateHues(a, b, t float64, hueMethod hueMethod) float64 {
a /= 360
b /= 360
a -= math.Floor(a)
b -= math.Floor(b)
switch hueMethod {
case shorterHue:
delta := b - a
if delta > 0.5 {
a++
}
if delta < -0.5 {
b++
}
case longerHue:
delta := b - a
if delta > 0 && delta < 0.5 {
a++
}
if delta > -0.5 && delta <= 0 {
b++
}
case increasingHue:
if b < a {
b++
}
case decreasingHue:
if a < b {
a++
}
}
return (a + (b-a)*t) * 360
}
func interpolateColors(
a0, a1, a2 float64, b0, b1, b2 float64,
colorSpace colorSpace, hueMethod hueMethod, t float64,
) (v0 float64, v1 float64, v2 float64) {
v1 = a1 + (b1-a1)*t
switch colorSpace {
case colorSpace_hsl, colorSpace_hwb:
v2 = a2 + (b2-a2)*t
v0 = interpolateHues(a0, b0, t, hueMethod)
case colorSpace_lch, colorSpace_oklch:
v0 = a0 + (b0-a0)*t
v2 = interpolateHues(a2, b2, t, hueMethod)
default:
v0 = a0 + (b0-a0)*t
v2 = a2 + (b2-a2)*t
}
return v0, v1, v2
}
func interpolatePositions(a []valueWithUnit, b []valueWithUnit, t float64) (result []valueWithUnit) {
findUnit := func(unit string) int {
for i, x := range result {
if x.unit == unit {
return i
}
}
result = append(result, valueWithUnit{unit: unit})
return len(result) - 1
}
// "result += a * (1 - t)"
for _, term := range a {
result[findUnit(term.unit)].value += term.value * (1 - t)
}
// "result += b * t"
for _, term := range b {
result[findUnit(term.unit)].value += term.value * t
}
// Remove an extra zero value for neatness. We don't remove all
// of them because it may be important to retain a single zero.
if len(result) > 1 {
for i, term := range result {
if term.value == 0 {
copy(result[i:], result[i+1:])
result = result[:len(result)-1]
break
}
}
}
return
}
func premultiply(v0, v1, v2, alpha float64, colorSpace colorSpace) (float64, float64, float64) {
if alpha < 1 {
switch colorSpace {
case colorSpace_hsl, colorSpace_hwb:
v2 *= alpha
case colorSpace_lch, colorSpace_oklch:
v0 *= alpha
default:
v0 *= alpha
v2 *= alpha
}
v1 *= alpha
}
return v0, v1, v2
}
func unpremultiply(v0, v1, v2, alpha float64, colorSpace colorSpace) (float64, float64, float64) {
if alpha > 0 && alpha < 1 {
switch colorSpace {
case colorSpace_hsl, colorSpace_hwb:
v2 /= alpha
case colorSpace_lch, colorSpace_oklch:
v0 /= alpha
default:
v0 /= alpha
v2 /= alpha
}
v1 /= alpha
}
return v0, v1, v2
}
+80 -27
View File
@@ -761,50 +761,74 @@ func TestGradient(t *testing.T) {
expectPrinted(t, code, "a {\n background: "+gradient+"(yellow, #11223344);\n}\n", "")
expectPrintedMangle(t, code, "a {\n background: "+gradient+"(#ff0, #1234);\n}\n", "")
expectPrintedMinify(t, code, "a{background:"+gradient+"(yellow,#11223344)}", "")
expectPrintedLower(t, code, "a {\n background: "+gradient+"(yellow, rgba(17, 34, 51, .267));\n}\n", "")
expectPrintedLowerUnsupported(t, compat.HexRGBA, code,
"a {\n background: "+gradient+"(yellow, rgba(17, 34, 51, .267));\n}\n", "")
// Basic with positions
code = "a { background: " + gradient + "(yellow 10%, #11223344 90%) }"
expectPrinted(t, code, "a {\n background: "+gradient+"(yellow 10%, #11223344 90%);\n}\n", "")
expectPrintedMangle(t, code, "a {\n background: "+gradient+"(#ff0 10%, #1234 90%);\n}\n", "")
expectPrintedMinify(t, code, "a{background:"+gradient+"(yellow 10%,#11223344 90%)}", "")
expectPrintedLower(t, code, "a {\n background: "+gradient+"(yellow 10%, rgba(17, 34, 51, .267) 90%);\n}\n", "")
expectPrintedLowerUnsupported(t, compat.HexRGBA, code,
"a {\n background: "+gradient+"(yellow 10%, rgba(17, 34, 51, .267) 90%);\n}\n", "")
// Basic with hints
code = "a { background: " + gradient + "(yellow, 25%, #11223344) }"
expectPrinted(t, code, "a {\n background: "+gradient+"(yellow, 25%, #11223344);\n}\n", "")
expectPrintedMangle(t, code, "a {\n background: "+gradient+"(#ff0, 25%, #1234);\n}\n", "")
expectPrinted(t, code, "a {\n background:\n "+gradient+"(\n yellow,\n 25%,\n #11223344);\n}\n", "")
expectPrintedMangle(t, code, "a {\n background:\n "+gradient+"(\n #ff0,\n 25%,\n #1234);\n}\n", "")
expectPrintedMinify(t, code, "a{background:"+gradient+"(yellow,25%,#11223344)}", "")
expectPrintedLower(t, code, "a {\n background: "+gradient+"(yellow, 25%, rgba(17, 34, 51, .267));\n}\n", "")
expectPrintedLowerUnsupported(t, compat.HexRGBA, code,
"a {\n background:\n "+gradient+"(\n yellow,\n 25%,\n rgba(17, 34, 51, .267));\n}\n", "")
expectPrintedLowerUnsupported(t, compat.GradientMidpoints, code,
"a {\n background:\n "+gradient+"(\n #ffff00,\n #f2f303de 3.12%,\n #eced04d0 6.25%,\n "+
"#e1e306bd 12.5%,\n #cdd00ba2 25%,\n #a2a8147b 50%,\n #6873205d 75%,\n #11223344);\n}\n", "")
// Double positions
code = "a { background: " + gradient + "(green, red 10%, red 20%, yellow 70% 80%, black) }"
expectPrinted(t, code, "a {\n background: "+gradient+"(green, red 10%, red 20%, yellow 70% 80%, black);\n}\n", "")
expectPrintedMangle(t, code, "a {\n background: "+gradient+"(green, red 10% 20%, #ff0 70% 80%, #000);\n}\n", "")
expectPrinted(t, code, "a {\n background:\n "+gradient+"(\n green,\n red 10%,\n "+
"red 20%,\n yellow 70% 80%,\n black);\n}\n", "")
expectPrintedMangle(t, code, "a {\n background:\n "+gradient+"(\n green,\n "+
"red 10% 20%,\n #ff0 70% 80%,\n #000);\n}\n", "")
expectPrintedMinify(t, code, "a{background:"+gradient+"(green,red 10%,red 20%,yellow 70% 80%,black)}", "")
expectPrintedLower(t, code, "a {\n background: "+gradient+"(green, red 10%, red 20%, yellow 70%, yellow 80%, black);\n}\n", "")
expectPrintedLowerUnsupported(t, compat.GradientDoublePosition, code,
"a {\n background:\n "+gradient+"(\n green,\n red 10%,\n red 20%,\n "+
"yellow 70%,\n yellow 80%,\n black);\n}\n", "")
// Double positions with hints
code = "a { background: " + gradient + "(green, red 10%, red 20%, 30%, yellow 70% 80%, 85%, black) }"
expectPrinted(t, code, "a {\n background: "+gradient+"(green, red 10%, red 20%, 30%, yellow 70% 80%, 85%, black);\n}\n", "")
expectPrintedMangle(t, code, "a {\n background: "+gradient+"(green, red 10% 20%, 30%, #ff0 70% 80%, 85%, #000);\n}\n", "")
expectPrinted(t, code, "a {\n background:\n "+gradient+"(\n green,\n red 10%,\n red 20%,\n "+
"30%,\n yellow 70% 80%,\n 85%,\n black);\n}\n", "")
expectPrintedMangle(t, code, "a {\n background:\n "+gradient+"(\n green,\n red 10% 20%,\n "+
"30%,\n #ff0 70% 80%,\n 85%,\n #000);\n}\n", "")
expectPrintedMinify(t, code, "a{background:"+gradient+"(green,red 10%,red 20%,30%,yellow 70% 80%,85%,black)}", "")
expectPrintedLower(t, code, "a {\n background: "+gradient+"(green, red 10%, red 20%, 30%, yellow 70%, yellow 80%, 85%, black);\n}\n", "")
expectPrintedLowerUnsupported(t, compat.GradientDoublePosition, code,
"a {\n background:\n "+gradient+"(\n green,\n red 10%,\n red 20%,\n 30%,\n "+
"yellow 70%,\n yellow 80%,\n 85%,\n black);\n}\n", "")
// Non-double positions with hints
code = "a { background: " + gradient + "(green, red 10%, 1%, red 20%, black) }"
expectPrinted(t, code, "a {\n background: "+gradient+"(green, red 10%, 1%, red 20%, black);\n}\n", "")
expectPrintedMangle(t, code, "a {\n background: "+gradient+"(green, red 10%, 1%, red 20%, #000);\n}\n", "")
expectPrinted(t, code, "a {\n background:\n "+gradient+"(\n green,\n red 10%,\n 1%,\n "+
"red 20%,\n black);\n}\n", "")
expectPrintedMangle(t, code, "a {\n background:\n "+gradient+"(\n green,\n red 10%,\n "+
"1%,\n red 20%,\n #000);\n}\n", "")
expectPrintedMinify(t, code, "a{background:"+gradient+"(green,red 10%,1%,red 20%,black)}", "")
expectPrintedLower(t, code, "a {\n background: "+gradient+"(green, red 10%, 1%, red 20%, black);\n}\n", "")
expectPrintedLowerUnsupported(t, compat.GradientDoublePosition, code,
"a {\n background:\n "+gradient+"(\n green,\n red 10%,\n 1%,\n red 20%,\n black);\n}\n", "")
// Out-of-gamut colors
code = "a { background: " + gradient + "(yellow, color(display-p3 1 0 0)) }"
expectPrinted(t, code, "a {\n background: "+gradient+"(yellow, color(display-p3 1 0 0));\n}\n", "")
expectPrintedMangle(t, code, "a {\n background: "+gradient+"(#ff0, color(display-p3 1 0 0));\n}\n", "")
expectPrintedMinify(t, code, "a{background:"+gradient+"(yellow,color(display-p3 1 0 0))}", "")
expectPrintedLower(t, code, "a {\n background: "+gradient+"(yellow, #ff0f0e);\n "+
"background: "+gradient+"(yellow, color(display-p3 1 0 0));\n}\n", "")
expectPrintedLowerUnsupported(t, compat.ColorFunctions, code,
"a {\n background:\n "+gradient+"(\n #ffff00,\n #ffe971 12.5%,\n #ffd472 25%,\n "+
"#ffab5f 50%,\n #ff7b45 75%,\n #ff5e38 87.5%,\n #ff5534 90.62%,\n #ff4c30 93.75%,\n "+
"#ff412c 96.88%,\n #ff0e0e);\n "+
"background:\n "+gradient+"(\n #ffff00,\n color(xyz 0.734 0.805 0.111) 12.5%,\n "+
"color(xyz 0.699 0.693 0.087) 25%,\n color(xyz 0.627 0.501 0.048) 50%,\n "+
"color(xyz 0.556 0.348 0.019) 75%,\n color(xyz 0.521 0.284 0.009) 87.5%,\n "+
"color(xyz 0.512 0.27 0.006) 90.62%,\n color(xyz 0.504 0.256 0.004) 93.75%,\n "+
"color(xyz 0.495 0.242 0.002) 96.88%,\n color(xyz 0.487 0.229 0));\n}\n", "")
// Whitespace
code = "a { background: " + gradient + "(color-mix(in lab,red,green)calc(1px)calc(2px),color-mix(in lab,blue,red)calc(98%)calc(99%)) }"
@@ -814,12 +838,40 @@ func TestGradient(t *testing.T) {
"(color-mix(in lab, red, green) 1px 2px, color-mix(in lab, blue, red) 98% 99%);\n}\n", "")
expectPrintedMinify(t, code, "a{background:"+gradient+
"(color-mix(in lab,red,green)calc(1px)calc(2px),color-mix(in lab,blue,red)calc(98%)calc(99%))}", "")
expectPrintedLower(t, code, "a {\n background: "+gradient+
"(color-mix(in lab, red, green) calc(1px), color-mix(in lab, red, green) calc(2px),"+
" color-mix(in lab, blue, red) calc(98%), color-mix(in lab, blue, red) calc(99%));\n}\n", "")
expectPrintedLowerMangle(t, code, "a {\n background: "+gradient+
"(color-mix(in lab, red, green) 1px, color-mix(in lab, red, green) 2px,"+
" color-mix(in lab, blue, red) 98%, color-mix(in lab, blue, red) 99%);\n}\n", "")
expectPrintedLowerUnsupported(t, compat.GradientDoublePosition, code, "a {\n background:\n "+gradient+
"(\n color-mix(in lab, red, green) calc(1px),\n color-mix(in lab, red, green) calc(2px),"+
"\n color-mix(in lab, blue, red) calc(98%),\n color-mix(in lab, blue, red) calc(99%));\n}\n", "")
expectPrintedLowerMangle(t, code, "a {\n background:\n "+gradient+
"(\n color-mix(in lab, red, green) 1px,\n color-mix(in lab, red, green) 2px,"+
"\n color-mix(in lab, blue, red) 98%,\n color-mix(in lab, blue, red) 99%);\n}\n", "")
// Color space interpolation
expectPrintedLowerUnsupported(t, compat.GradientInterpolation,
"a { background: "+gradient+"(in srgb, red, green) }",
"a {\n background: "+gradient+"(#ff0000, #008000);\n}\n", "")
expectPrintedLowerUnsupported(t, compat.GradientInterpolation,
"a { background: "+gradient+"(in srgb-linear, red, green) }",
"a {\n background:\n "+gradient+"(\n #ff0000,\n #fb1300 3.12%,\n #f81f00 6.25%,\n "+
"#f02e00 12.5%,\n #e14200 25%,\n #bc5c00 50%,\n #897000 75%,\n #637800 87.5%,\n "+
"#477c00 93.75%,\n #317e00 96.88%,\n #008000);\n}\n", "")
expectPrintedLowerUnsupported(t, compat.GradientInterpolation,
"a { background: "+gradient+"(in lab, red, green) }",
"a {\n background:\n "+gradient+"(\n #ff0000,\n color(xyz 0.396 0.211 0.019) 3.12%,\n "+
"color(xyz 0.38 0.209 0.02) 6.25%,\n color(xyz 0.35 0.205 0.02) 12.5%,\n "+
"color(xyz 0.294 0.198 0.02) 25%,\n color(xyz 0.2 0.183 0.022) 50%,\n "+
"color(xyz 0.129 0.168 0.024) 75%,\n color(xyz 0.101 0.161 0.025) 87.5%,\n "+
"color(xyz 0.089 0.158 0.025) 93.75%,\n color(xyz 0.083 0.156 0.025) 96.88%,\n #008000);\n}\n", "")
// Hue interpolation
expectPrintedLowerUnsupported(t, compat.GradientInterpolation,
"a { background: "+gradient+"(in hsl shorter hue, red, green) }",
"a {\n background:\n "+gradient+"(\n #ff0000,\n #df7000 25%,\n "+
"#bfbf00 50%,\n #50a000 75%,\n #008000);\n}\n", "")
expectPrintedLowerUnsupported(t, compat.GradientInterpolation,
"a { background: "+gradient+"(in hsl longer hue, red, green) }",
"a {\n background:\n "+gradient+"(\n #ff0000,\n #ef0078 12.5%,\n "+
"#df00df 25%,\n #6800cf 37.5%,\n #0000c0 50%,\n #0058b0 62.5%,\n "+
"#00a0a0 75%,\n #009048 87.5%,\n #008000);\n}\n", "")
}
}
@@ -2145,7 +2197,8 @@ func TestTransform(t *testing.T) {
expectPrintedMangle(t, "a { transform: matrix(1, 0, 0, 2, 0, 0) }", "a {\n transform: scaleY(2);\n}\n", "")
expectPrintedMangle(t, "a { transform: matrix(2, 0, 0, 3, 0, 0) }", "a {\n transform: scale(2, 3);\n}\n", "")
expectPrintedMangle(t, "a { transform: matrix(2, 0, 0, 2, 0, 0) }", "a {\n transform: scale(2);\n}\n", "")
expectPrintedMangle(t, "a { transform: matrix(1, 0, 0, 1, 1, 2) }", "a {\n transform: matrix(1, 0, 0, 1, 1, 2);\n}\n", "")
expectPrintedMangle(t, "a { transform: matrix(1, 0, 0, 1, 1, 2) }",
"a {\n transform:\n matrix(\n 1, 0,\n 0, 1,\n 1, 2);\n}\n", "")
expectPrintedMangle(t, "a { transform: translate(0, 0) }", "a {\n transform: translate(0);\n}\n", "")
expectPrintedMangle(t, "a { transform: translate(0px, 0px) }", "a {\n transform: translate(0);\n}\n", "")
@@ -2218,13 +2271,13 @@ func TestTransform(t *testing.T) {
expectPrintedMangle(t,
"a { transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2) }",
"a {\n transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2);\n}\n", "")
"a {\n transform:\n matrix3d(\n 1, 0, 0, 0,\n 0, 1, 0, 0,\n 0, 0, 1, 0,\n 0, 0, 0, 2);\n}\n", "")
expectPrintedMangle(t,
"a { transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 2, 3, 4, 1) }",
"a {\n transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 2, 3, 4, 1);\n}\n", "")
"a {\n transform:\n matrix3d(\n 1, 0, 0, 0,\n 0, 1, 0, 0,\n 0, 0, 1, 0,\n 2, 3, 4, 1);\n}\n", "")
expectPrintedMangle(t,
"a { transform: matrix3d(1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1) }",
"a {\n transform: matrix3d(1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1);\n}\n", "")
"a {\n transform:\n matrix3d(\n 1, 0, 1, 0,\n 0, 1, 0, 0,\n 1, 0, 1, 0,\n 0, 0, 0, 1);\n}\n", "")
expectPrintedMangle(t,
"a { transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) }",
"a {\n transform: scaleZ(1);\n}\n", "")
@@ -2248,7 +2301,7 @@ func TestTransform(t *testing.T) {
"a {\n transform: scale3d(1, 2, 3);\n}\n", "")
expectPrintedMangle(t,
"a { transform: matrix3d(2, 3, 0, 0, 4, 5, 0, 0, 0, 0, 1, 0, 6, 7, 0, 1) }",
"a {\n transform: matrix3d(2, 3, 0, 0, 4, 5, 0, 0, 0, 0, 1, 0, 6, 7, 0, 1);\n}\n", "")
"a {\n transform:\n matrix3d(\n 2, 3, 0, 0,\n 4, 5, 0, 0,\n 0, 0, 1, 0,\n 6, 7, 0, 1);\n}\n", "")
expectPrintedMangle(t, "a { transform: translate3d(0, 0, 0) }", "a {\n transform: translateZ(0);\n}\n", "")
expectPrintedMangle(t, "a { transform: translate3d(0%, 0%, 0) }", "a {\n transform: translateZ(0);\n}\n", "")
+69 -6
View File
@@ -942,32 +942,74 @@ func (p *printer) printIndent(indent int32) {
}
type printTokensOpts struct {
indent int32
isDeclaration bool
indent int32
multiLineCommaPeriod uint8
isDeclaration bool
}
func functionMultiLineCommaPeriod(token css_ast.Token) uint8 {
if token.Kind == css_lexer.TFunction {
commaCount := 0
for _, t := range *token.Children {
if t.Kind == css_lexer.TComma {
commaCount++
}
}
switch strings.ToLower(token.Text) {
case "linear-gradient", "radial-gradient", "conic-gradient",
"repeating-linear-gradient", "repeating-radial-gradient", "repeating-conic-gradient":
if commaCount >= 2 {
return 1
}
case "matrix":
if commaCount == 5 {
return 2
}
case "matrix3d":
if commaCount == 15 {
return 4
}
}
}
return 0
}
func (p *printer) printTokens(tokens []css_ast.Token, opts printTokensOpts) bool {
hasWhitespaceAfter := len(tokens) > 0 && (tokens[0].Whitespace&css_ast.WhitespaceBefore) != 0
// Pretty-print long comma-separated declarations of 3 or more items
isMultiLineValue := false
commaPeriod := int(opts.multiLineCommaPeriod)
if !p.options.MinifyWhitespace && opts.isDeclaration {
commaCount := 0
for _, t := range tokens {
if t.Kind == css_lexer.TComma {
commaCount++
if commaCount >= 2 {
commaPeriod = 1
break
}
}
if t.Kind == css_lexer.TFunction && functionMultiLineCommaPeriod(t) > 0 {
commaPeriod = 1
break
}
}
isMultiLineValue = commaCount >= 2
}
commaCount := 0
for i, t := range tokens {
if t.Kind == css_lexer.TComma {
commaCount++
}
if t.Kind == css_lexer.TWhitespace {
hasWhitespaceAfter = true
continue
}
if hasWhitespaceAfter {
if isMultiLineValue && (i == 0 || tokens[i-1].Kind == css_lexer.TComma) {
if commaPeriod > 0 && (i == 0 || (tokens[i-1].Kind == css_lexer.TComma && commaCount%commaPeriod == 0)) {
p.print("\n")
p.printIndent(opts.indent + 1)
} else if p.options.LineLimit <= 0 || !p.printNewlinePastLineLimit(opts.indent+1) {
@@ -1054,7 +1096,28 @@ func (p *printer) printTokens(tokens []css_ast.Token, opts printTokensOpts) bool
}
if t.Children != nil {
p.printTokens(*t.Children, printTokensOpts{indent: opts.indent})
childCommaPeriod := uint8(0)
if commaPeriod > 0 && opts.isDeclaration {
childCommaPeriod = functionMultiLineCommaPeriod(t)
}
if childCommaPeriod > 0 {
opts.indent++
if !p.options.MinifyWhitespace {
p.print("\n")
p.printIndent(opts.indent + 1)
}
}
p.printTokens(*t.Children, printTokensOpts{
indent: opts.indent,
multiLineCommaPeriod: childCommaPeriod,
})
if childCommaPeriod > 0 {
opts.indent--
}
switch t.Kind {
case css_lexer.TFunction:
+95
View File
@@ -0,0 +1,95 @@
.linear.red_to_green-esbuild {
background: linear-gradient(to right, color(display-p3 1 0 0), color(display-p3 0 0.6 0));
}
.radial.red_to_green-esbuild {
background: radial-gradient(color(display-p3 1 0 0), color(display-p3 0 0.6 0));
}
.conic.red_to_green-esbuild {
background: conic-gradient(color(display-p3 1 0 0), color(display-p3 0 0.6 0));
}
.linear.rainbow_shorter-esbuild {
background: linear-gradient(to right in hwb shorter hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.radial.rainbow_shorter-esbuild {
background: radial-gradient(in hwb shorter hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.conic.rainbow_shorter-esbuild {
background: conic-gradient(in hwb shorter hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.linear.rainbow_longer-esbuild {
background: linear-gradient(to right in hsl longer hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.radial.rainbow_longer-esbuild {
background: radial-gradient(in hsl longer hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.conic.rainbow_longer-esbuild {
background: conic-gradient(in hsl longer hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.linear.rainbow_increasing-esbuild {
background: linear-gradient(to right in lch increasing hue, hsl(240deg 100% 75%) 10%, hsl(180deg 100% 75%) 90%);
}
.radial.rainbow_increasing-esbuild {
background: radial-gradient(in lch increasing hue, hsl(240deg 100% 75%) 10%, hsl(180deg 100% 75%) 90%);
}
.conic.rainbow_increasing-esbuild {
background: conic-gradient(in lch increasing hue, hsl(240deg 100% 75%) 10%, hsl(180deg 100% 75%) 90%);
}
.linear.rainbow_decreasing-esbuild {
background: linear-gradient(to right in oklch decreasing hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.radial.rainbow_decreasing-esbuild {
background: radial-gradient(in oklch decreasing hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.conic.rainbow_decreasing-esbuild {
background: conic-gradient(in oklch decreasing hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.linear.midpoint_hint-esbuild {
background: linear-gradient(to right, #f00, #ff0, 75%, #0ff, #00f);
}
.radial.midpoint_hint-esbuild {
background: radial-gradient(#f00, #ff0, 75%, #0ff, #00f);
}
.conic.midpoint_hint-esbuild {
background: conic-gradient(#f00, #ff0, 75%, #0ff, #00f);
}
.linear.premultiplied_alpha-esbuild {
background: linear-gradient(to right, #f00f, 10%, #00f1, 90%, #0f0f);
}
.radial.premultiplied_alpha-esbuild {
background: radial-gradient(#f00f, 10%, #00f1, 90%, #0f0f);
}
.conic.premultiplied_alpha-esbuild {
background: conic-gradient(#f00f, 10%, #00f1, 90%, #0f0f);
}
.linear.mixed_units-esbuild {
background: linear-gradient(to right, color(display-p3 0.4 0 1) 30px, color(display-p3 0.96 0.75 0.4) 60%);
}
.radial.mixed_units-esbuild {
background: radial-gradient(color(display-p3 0.4 0 1) 30px, color(display-p3 1 0.75 0.4) 60%);
}
.conic.mixed_units-esbuild {
background: conic-gradient(color(display-p3 0.4 0 1) 30deg, color(display-p3 1 0.75 0.4) 60%);
}
+301
View File
@@ -0,0 +1,301 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<link rel="stylesheet" href="out.css">
<style>
body {
margin: 30px;
font: 16px/20px sans-serif;
color: black;
background: white;
}
@media (prefers-color-scheme: dark) {
body {
color: white;
background: black;
}
}
a {
color: inherit;
}
p {
margin: 20px 0;
}
pre {
margin: 20px 0 10px 0;
font: 14px/20px Monaco, monospace;
white-space: pre-wrap;
}
h2 {
margin: 50px 0 0 0;
}
.linear {
max-width: 400px;
height: 20px;
text-indent: 4px;
font-size: 14px;
color: white;
text-shadow: 0 0 3px black;
background: repeating-linear-gradient(-45deg,
rgba(191, 191, 191, 0.4) 0 7px,
transparent 7px 14px);
}
section {
margin-top: 12px;
display: flex;
}
.radial+.radial {
margin-right: 12px;
}
.radial,
.conic {
width: 97px;
height: 97px;
text-align: center;
line-height: 97px;
font-size: 14px;
color: white;
text-shadow: 0 0 3px black;
background: repeating-linear-gradient(-45deg,
transparent 0 6.86px,
rgba(191, 191, 191, 0.4) 6.86px 13.72px);
}
.checkerboard {
position: relative;
}
.checkerboard:after {
content: ' ';
display: block;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1;
background: url('data:image/svg+xml,<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><rect width="20" height="20" fill="white"/><path d="M0 10V0H10V10H20V20H10V10H0Z" fill="%23EEEEEE"/></svg>');
}
.linear.red_to_green-native {
background: linear-gradient(to right, color(display-p3 1 0 0), color(display-p3 0 0.6 0));
}
.radial.red_to_green-native {
background: radial-gradient(color(display-p3 1 0 0), color(display-p3 0 0.6 0));
}
.conic.red_to_green-native {
background: conic-gradient(color(display-p3 1 0 0), color(display-p3 0 0.6 0));
}
.linear.rainbow_shorter-native {
background: linear-gradient(to right in hwb shorter hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.radial.rainbow_shorter-native {
background: radial-gradient(in hwb shorter hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.conic.rainbow_shorter-native {
background: conic-gradient(in hwb shorter hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.linear.rainbow_longer-native {
background: linear-gradient(to right in hsl longer hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.radial.rainbow_longer-native {
background: radial-gradient(in hsl longer hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.conic.rainbow_longer-native {
background: conic-gradient(in hsl longer hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.linear.rainbow_increasing-native {
background: linear-gradient(to right in lch increasing hue, hsl(240deg 100% 75%) 10%, hsl(180deg 100% 75%) 90%);
}
.radial.rainbow_increasing-native {
background: radial-gradient(in lch increasing hue, hsl(240deg 100% 75%) 10%, hsl(180deg 100% 75%) 90%);
}
.conic.rainbow_increasing-native {
background: conic-gradient(in lch increasing hue, hsl(240deg 100% 75%) 10%, hsl(180deg 100% 75%) 90%);
}
.linear.rainbow_decreasing-native {
background: linear-gradient(to right in oklch decreasing hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.radial.rainbow_decreasing-native {
background: radial-gradient(in oklch decreasing hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.conic.rainbow_decreasing-native {
background: conic-gradient(in oklch decreasing hue, hsl(180deg 100% 75%) 10%, hsl(240deg 100% 75%) 90%);
}
.linear.midpoint_hint-native {
background: linear-gradient(to right, #f00, #ff0, 75%, #0ff, #00f);
}
.radial.midpoint_hint-native {
background: radial-gradient(#f00, #ff0, 75%, #0ff, #00f);
}
.conic.midpoint_hint-native {
background: conic-gradient(#f00, #ff0, 75%, #0ff, #00f);
}
.linear.premultiplied_alpha-native {
background: linear-gradient(to right, #f00f, 10%, #00f1, 90%, #0f0f);
}
.radial.premultiplied_alpha-native {
background: radial-gradient(#f00f, 10%, #00f1, 90%, #0f0f);
}
.conic.premultiplied_alpha-native {
background: conic-gradient(#f00f, 10%, #00f1, 90%, #0f0f);
}
.linear.mixed_units-native {
background: linear-gradient(to right, color(display-p3 0.4 0 1) 30px, color(display-p3 1 0.75 0.4) 60%);
}
.radial.mixed_units-native {
background: radial-gradient(color(display-p3 0.4 0 1) 30px, color(display-p3 1 0.75 0.4) 60%);
}
.conic.mixed_units-native {
background: conic-gradient(color(display-p3 0.4 0 1) 30deg, color(display-p3 1 0.75 0.4) 60%);
}
</style>
</head>
<body>
<p>This page is a visual test of esbuild's CSS gradient transformation. Run it like this:</p>
<pre>./esbuild --servedir=scripts --outfile=scripts/out.css scripts/gradient-tests.css --target=firefox30</pre>
<p>Then open this page as <a
href="http://localhost:8000/gradient-tests.html">http://localhost:8000/gradient-tests.html</a>.</p>
<h2>1. Red to green in P3</h2>
<pre>gradient(
color(display-p3 1 0 0),
color(display-p3 0 0.6 0))</pre>
<div class="linear red_to_green-native">native</div>
<div class="linear red_to_green-esbuild">esbuild</div>
<section>
<div class="radial red_to_green-native">native</div>
<div class="radial red_to_green-esbuild">esbuild</div>
<div class="conic red_to_green-native">native</div>
<div class="conic red_to_green-esbuild">esbuild</div>
</section>
<h2>2. Rainbow using shorter hue</h2>
<pre>gradient(
in hwb shorter hue,
hsl(180deg 100% 75%) 10%,
hsl(240deg 100% 75%) 90%)</pre>
<div class="linear rainbow_shorter-native">native</div>
<div class="linear rainbow_shorter-esbuild">esbuild</div>
<section>
<div class="radial rainbow_shorter-native">native</div>
<div class="radial rainbow_shorter-esbuild">esbuild</div>
<div class="conic rainbow_shorter-native">native</div>
<div class="conic rainbow_shorter-esbuild">esbuild</div>
</section>
<h2>3. Rainbow using longer hue</h2>
<pre>gradient(
in hsl longer hue,
hsl(180deg 100% 75%) 10%,
hsl(240deg 100% 75%) 90%)</pre>
<div class="linear rainbow_longer-native">native</div>
<div class="linear rainbow_longer-esbuild">esbuild</div>
<section>
<div class="radial rainbow_longer-native">native</div>
<div class="radial rainbow_longer-esbuild">esbuild</div>
<div class="conic rainbow_longer-native">native</div>
<div class="conic rainbow_longer-esbuild">esbuild</div>
</section>
<h2>4. Rainbow using increasing hue</h2>
<pre>gradient(
in lch increasing hue,
hsl(240deg 100% 75%) 10%,
hsl(180deg 100% 75%) 90%)</pre>
<div class="linear rainbow_increasing-native">native</div>
<div class="linear rainbow_increasing-esbuild">esbuild</div>
<section>
<div class="radial rainbow_increasing-native">native</div>
<div class="radial rainbow_increasing-esbuild">esbuild</div>
<div class="conic rainbow_increasing-native">native</div>
<div class="conic rainbow_increasing-esbuild">esbuild</div>
</section>
<h2>5. Rainbow using decreasing hue</h2>
<pre>gradient(
in oklch decreasing hue,
hsl(180deg 100% 75%) 10%,
hsl(240deg 100% 75%) 90%)</pre>
<div class="linear rainbow_decreasing-native">native</div>
<div class="linear rainbow_decreasing-esbuild">esbuild</div>
<section>
<div class="radial rainbow_decreasing-native">native</div>
<div class="radial rainbow_decreasing-esbuild">esbuild</div>
<div class="conic rainbow_decreasing-native">native</div>
<div class="conic rainbow_decreasing-esbuild">esbuild</div>
</section>
<h2>6. Transition hint / midpoint</h2>
<pre>gradient(#f00, #ff0, 75%, #0ff, #00f)</pre>
<div class="linear midpoint_hint-native">native</div>
<div class="linear midpoint_hint-esbuild">esbuild</div>
<section>
<div class="radial midpoint_hint-native">native</div>
<div class="radial midpoint_hint-esbuild">esbuild</div>
<div class="conic midpoint_hint-native">native</div>
<div class="conic midpoint_hint-esbuild">esbuild</div>
</section>
<h2>7. Premultiplied alpha</h2>
<pre>gradient(#f00f, 10%, #00f1, 90%, #0f0f)</pre>
<div class="checkerboard linear premultiplied_alpha-native">native</div>
<div class="checkerboard linear premultiplied_alpha-esbuild">esbuild</div>
<section>
<div class="checkerboard radial premultiplied_alpha-native">native</div>
<div class="checkerboard radial premultiplied_alpha-esbuild">esbuild</div>
<div class="checkerboard conic premultiplied_alpha-native">native</div>
<div class="checkerboard conic premultiplied_alpha-esbuild">esbuild</div>
</section>
<h2>8. Mixed units</h2>
<pre>gradient(
color(display-p3 0.4 0 1) 30px,
color(display-p3 1 0.75 0.4) 60%)</pre>
<div class="linear mixed_units-native">native</div>
<div class="linear mixed_units-esbuild">esbuild</div>
<section>
<div class="radial mixed_units-native">native</div>
<div class="radial mixed_units-esbuild">esbuild</div>
<div class="conic mixed_units-native">native</div>
<div class="conic mixed_units-esbuild">esbuild</div>
</section>
</body>
</html>