at-property-animation.html (15487B)
1 <!DOCTYPE html> 2 <link rel="help" href="https://drafts.css-houdini.org/css-properties-values-api-1"> 3 <script src="/resources/testharness.js"></script> 4 <script src="/resources/testharnessreport.js"></script> 5 <script src="./resources/utils.js"></script> 6 <div id=outer> 7 <div id=div></div> 8 </div> 9 <script> 10 11 test_with_at_property({ 12 syntax: '"<length>"', 13 inherits: false, 14 initialValue: '0px' 15 }, (name) => { 16 with_style_node(` 17 @keyframes test { 18 from { ${name}: 100px; } 19 to { ${name}: 200px; } 20 } 21 #div { animation: test 100s -50s linear; } 22 `, () => { 23 assert_equals(getComputedStyle(div).getPropertyValue(name), '150px'); 24 }); 25 }, '@keyframes works with @property'); 26 27 test_with_at_property({ 28 syntax: '"<length>"', 29 inherits: false, 30 initialValue: '0px' 31 }, (name) => { 32 with_style_node(` 33 @property ${name} { 34 syntax: "<color>"; 35 inherits: false; 36 initial-value: black; 37 } 38 @keyframes test { 39 from { ${name}: rgb(100, 100, 100); } 40 to { ${name}: rgb(200, 200, 200); } 41 } 42 #div { animation: test 100s -50s linear; } 43 `, () => { 44 assert_equals(getComputedStyle(div).getPropertyValue(name), 'rgb(150, 150, 150)'); 45 }); 46 }, '@keyframes picks up the latest @property in the document'); 47 48 test_with_at_property({ 49 syntax: '"<length>"', 50 inherits: false, 51 initialValue: '0px' 52 }, (name) => { 53 // These keyframes are initially invalid for the declared custom property. 54 let animation = div.animate([ 55 { [name]: 'rgb(100, 100, 100)'}, 56 { [name]: 'rgb(200, 200, 200)'}, 57 ], { duration: 10000, delay: -5000, easing: 'linear' }); 58 let cs = getComputedStyle(div); 59 assert_equals(cs.getPropertyValue(name), '0px'); 60 61 // Redeclare the property as a <color>, effectively making the existing 62 // keyframes valid. 63 with_at_property({ 64 name: name, 65 syntax: '"<color>"', 66 inherits: false, 67 initialValue: 'black' 68 }, (name) => { 69 assert_equals(cs.getPropertyValue(name), 'rgb(150, 150, 150)'); 70 }); 71 72 animation.finish(); 73 }, 'Ongoing animation picks up redeclared custom property'); 74 75 test_with_at_property({ 76 syntax: '"<length>"', 77 inherits: false, 78 initialValue: '0px' 79 }, (name) => { 80 // These keyframes are initially invalid for the declared custom property. 81 let animation = div.animate([ 82 { [name]: 'rgb(100, 100, 100)'}, 83 { [name]: 'rgb(200, 200, 200)'}, 84 ], { duration: 10000, delay: -5000, easing: 'linear' }); 85 let cs = getComputedStyle(div); 86 assert_equals(cs.getPropertyValue(name), '0px'); 87 88 // Setting the keyframes to something that matches <length> makes the 89 // interpolation valid. 90 animation.effect.setKeyframes([ 91 {[name]: '100px'}, 92 {[name]: '200px'} 93 ]); 94 assert_equals(cs.getPropertyValue(name), '150px'); 95 96 animation.finish(); 97 }, 'Ongoing animation matches new keyframes against the current registration'); 98 99 test_with_at_property({ 100 syntax: '"<length>"', 101 inherits: false, 102 initialValue: '0px' 103 }, (name) => { 104 let animation = div.animate([ 105 { [name]: 'initial'}, 106 { [name]: '400px'}, 107 ], { duration: 10000, delay: -5000, easing: 'linear' }); 108 let cs = getComputedStyle(div); 109 assert_equals(cs.getPropertyValue(name), '200px'); 110 111 // Change initial value. 112 with_at_property({ 113 name: name, 114 syntax: '"<length>"', 115 inherits: false, 116 initialValue: '100px' 117 }, (name) => { 118 assert_equals(cs.getPropertyValue(name), '250px'); 119 }); 120 121 animation.finish(); 122 }, 'Ongoing animation picks up redeclared intial value'); 123 124 test_with_at_property({ 125 syntax: '"<length>"', 126 inherits: false, 127 initialValue: '0px' 128 }, (name) => { 129 try { 130 document.body.style = `${name}: 100px`; 131 // Note that 'inherit' here refers to #outer, which has the initial 132 // value. (#outer did not inherit from body, since the property is not 133 // yet declared as inherited). 134 let animation = div.animate([ 135 { [name]: 'inherit'}, 136 { [name]: '400px'}, 137 ], { duration: 10000, delay: -5000, easing: 'linear' }); 138 let cs = getComputedStyle(div); 139 assert_equals(cs.getPropertyValue(name), '200px'); 140 141 // Change inherits to 'true'. The value should now propagate from body 142 // to #outer. 143 with_at_property({ 144 name: name, 145 syntax: '"<length>"', 146 inherits: true, 147 initialValue: '0px' 148 }, (name) => { 149 assert_equals(cs.getPropertyValue(name), '250px'); 150 }); 151 152 animation.finish(); 153 } finally { 154 document.body.style = ''; 155 } 156 }, 'Ongoing animation picks up redeclared inherits flag'); 157 158 test_with_at_property({ 159 syntax: '"<length>"', 160 inherits: false, 161 initialValue: '0px' 162 }, (name) => { 163 try { 164 outer.style = `${name}: 100px`; 165 // 'unset' should take the initial value (not the value from #outer), since 166 // the property is not declared as inherited. 167 let animation = div.animate([ 168 { [name]: 'unset'}, 169 { [name]: '400px'}, 170 ], { duration: 10000, delay: -5000, easing: 'linear' }); 171 let cs = getComputedStyle(div); 172 assert_equals(cs.getPropertyValue(name), '200px'); 173 174 // Change inherits to 'true'. 'unset' now refers to #outer's value. 175 with_at_property({ 176 name: name, 177 syntax: '"<length>"', 178 inherits: true, 179 initialValue: '0px' 180 }, (name) => { 181 assert_equals(cs.getPropertyValue(name), '250px'); 182 }); 183 184 animation.finish(); 185 } finally { 186 outer.style = ''; 187 } 188 }, 'Ongoing animation picks up redeclared meaning of \'unset\''); 189 190 test_with_at_property({ 191 syntax: '"<color>"', 192 inherits: false, 193 initialValue: 'red' 194 }, (name) => { 195 try { 196 assert_equals(getComputedStyle(div).getPropertyValue(name), 'rgb(255, 0, 0)'); 197 div.style = `transition: ${name} steps(2, start) 100s; ${name}: blue`; 198 assert_equals(getComputedStyle(div).getPropertyValue(name), 'rgb(128, 0, 128)'); 199 } finally { 200 div.style = ''; 201 } 202 }, 'Transitioning from initial value'); 203 204 test_with_at_property({ 205 syntax: '"<color>"', 206 inherits: false, 207 initialValue: 'red' 208 }, (name) => { 209 try { 210 div.style = `${name}: blue;`; 211 assert_equals(getComputedStyle(div).getPropertyValue(name), 'rgb(0, 0, 255)'); 212 div.style = `transition: ${name} steps(2, start) 100s; ${name}: green`; 213 assert_equals(getComputedStyle(div).getPropertyValue(name), 'rgb(0, 64, 128)'); 214 } finally { 215 div.style = ''; 216 } 217 }, 'Transitioning from specified value'); 218 219 test_with_at_property({ 220 syntax: '"<length>"', 221 inherits: false, 222 initialValue: '100px' 223 }, (name) => { 224 with_style_node(`div { transition: ${name} steps(2, start) 100s; }`, () => { 225 assert_equals(getComputedStyle(div).getPropertyValue(name), '100px'); 226 // Re-declaring the property with a different initial value effectively 227 // means the computed value has changed. This means we should transition 228 // from the old initial value to the new initial value. 229 with_at_property({ 230 name: name, 231 syntax: '"<length>"', 232 inherits: false, 233 initialValue: '200px' 234 }, () => { 235 assert_equals(getComputedStyle(div).getPropertyValue(name), '150px'); 236 }); 237 }); 238 }, 'Transition triggered by initial value change'); 239 240 test_with_at_property({ 241 syntax: '"<length>"', 242 inherits: false, 243 initialValue: '100px' 244 }, (name) => { 245 with_style_node(`div { transition: ${name} steps(2, start) 100s; }`, () => { 246 assert_equals(getComputedStyle(div).getPropertyValue(name), '100px'); 247 with_at_property({ 248 name: name, 249 syntax: '"<color>"', 250 inherits: false, 251 initialValue: 'green' 252 }, () => { 253 assert_equals(getComputedStyle(div).getPropertyValue(name), 'rgb(0, 128, 0)'); 254 }); 255 }); 256 }, 'No transition when changing types'); 257 258 test(() => { 259 let name = generate_name(); 260 with_style_node(`div { ${name}: 100px; transition: ${name} steps(2, start) 100s; }`, () => { 261 assert_equals(getComputedStyle(div).getPropertyValue(name), '100px'); 262 263 let style1 = document.createElement('style'); 264 style1.textContent = ` 265 @property ${name} { 266 syntax: "<length>"; 267 inherits: false; 268 initial-value: 200px; 269 } 270 `; 271 272 let style2 = document.createElement('style'); 273 style2.textContent = `div { ${name}: 400px; }`; 274 275 try { 276 // Register the property: 277 document.body.append(style1); 278 // The token sequence ' 100px' is now interpreted as a length '100px'. 279 assert_equals(getComputedStyle(div).getPropertyValue(name), '100px'); 280 281 // Change the computed value: 282 document.body.append(style2); 283 // This should cause an interpolation between 100px and 400px: 284 assert_equals(getComputedStyle(div).getPropertyValue(name), '250px'); 285 286 // In the middle of the transition above, remove the @property rule 287 // (making the computed value a token sequence again). We should snap 288 // to the new token sequence. 289 style1.remove(); 290 assert_equals(getComputedStyle(div).getPropertyValue(name), '400px'); 291 } finally { 292 style1.remove(); 293 style2.remove(); 294 } 295 }); 296 }, 'No transition when removing @property rule'); 297 298 test(() => { 299 let name = generate_name(); 300 with_style_node(`div { transition: ${name} steps(2, start) 100s; }`, () => { 301 let style1 = document.createElement('style'); 302 style1.textContent = ` 303 @property ${name} { 304 syntax: "<angle>"; 305 inherits: false; 306 initial-value: -90deg; 307 } 308 `; 309 310 let style2 = document.createElement('style'); 311 style2.textContent = `div { ${name}: 90deg; }`; 312 313 try { 314 // Register the property: 315 document.body.append(style1); 316 // No transition in the beginning. 317 assert_equals(getComputedStyle(div).getPropertyValue(name), '-90deg'); 318 319 // Change the computed value: 320 document.body.append(style2); 321 // This should cause an interpolation between -90deg and 90deg (for more 322 // specifically, it is 50% from -90deg to 90deg, with steps(2, start)). 323 assert_equals(getComputedStyle(div).getPropertyValue(name), '0deg'); 324 325 // In the middle of the transition above, remove style2, which creates 326 // a transition which reverses the existing one, from 0deg to -90deg. 327 // Also, it is 50% from 0deg to -90deg, with steps(2, start). 328 style2.remove(); 329 assert_equals(getComputedStyle(div).getPropertyValue(name), '-45deg'); 330 } finally { 331 style1.remove(); 332 style2.remove(); 333 } 334 }); 335 }, 'Ongoing transition reverts to its initial state'); 336 337 test_with_at_property({ 338 syntax: '"<length>"', 339 inherits: false, 340 initialValue: '0px' 341 }, (name) => { 342 with_style_node(` 343 @keyframes test { 344 from { ${name}: 100px; } 345 to { ${name}: 200px; } 346 } 347 #div { 348 animation: test 100s -50s linear; 349 --unregistered: var(${name}); 350 } 351 `, () => { 352 assert_equals(getComputedStyle(div).getPropertyValue('--unregistered'), '150px'); 353 }); 354 }, 'Unregistered properties referencing animated properties update correctly.'); 355 356 test_with_at_property({ 357 syntax: '"<length>"', 358 inherits: false, 359 initialValue: '0px' 360 }, (name) => { 361 with_style_node(` 362 @keyframes test { 363 from { ${name}: 100px; } 364 to { ${name}: 200px; } 365 } 366 @property --registered { 367 syntax: "<length>"; 368 inherits: false; 369 initialValue: 0px; 370 } 371 #div { 372 animation: test 100s -50s linear; 373 --registered: var(${name}); 374 } 375 `, () => { 376 assert_equals(getComputedStyle(div).getPropertyValue('--registered'), '150px'); 377 }); 378 }, 'Registered properties referencing animated properties update correctly.'); 379 380 test_with_at_property({ 381 syntax: '"<length>"', 382 inherits: false, 383 initialValue: '0px' 384 }, (name) => { 385 with_style_node(` 386 @keyframes test { 387 from { ${name}: inherit; } 388 to { ${name}: 300px; } 389 } 390 #outer { 391 ${name}: 100px; 392 } 393 #div { 394 animation: test 100s -50s linear paused; 395 } 396 `, () => { 397 assert_equals(getComputedStyle(div).getPropertyValue(name), '200px'); 398 399 outer.style.setProperty(name, '200px'); 400 assert_equals(getComputedStyle(div).getPropertyValue(name), '250px'); 401 402 outer.style.setProperty(name, '0px'); 403 assert_equals(getComputedStyle(div).getPropertyValue(name), '150px'); 404 405 outer.style.removeProperty(name); 406 }); 407 }, 'CSS animation setting "inherit" for a custom property on a keyframe is responsive to changing that custom property on the parent.'); 408 409 test_with_at_property({ 410 syntax: '"<length>"', 411 inherits: false, 412 initialValue: '0px' 413 }, (name) => { 414 with_style_node(` 415 #outer { 416 ${name}: 100px; 417 } 418 `, () => { 419 const animation = div.animate({ [name]: ["inherit", "300px"]}, 1000); 420 animation.currentTime = 500; 421 animation.pause(); 422 423 assert_equals(getComputedStyle(div).getPropertyValue(name), '200px'); 424 425 outer.style.setProperty(name, '200px'); 426 assert_equals(getComputedStyle(div).getPropertyValue(name), '250px'); 427 428 outer.style.setProperty(name, '0px'); 429 assert_equals(getComputedStyle(div).getPropertyValue(name), '150px'); 430 431 outer.style.removeProperty(name); 432 }); 433 }, 'JS-originated animation setting "inherit" for a custom property on a keyframe is responsive to changing that custom property on the parent.'); 434 435 test_with_at_property({ 436 syntax: '"<color>"', 437 inherits: false, 438 initialValue: 'black' 439 }, (name) => { 440 with_style_node(` 441 @keyframes test { 442 from { ${name}: currentcolor; } 443 to { ${name}: rgb(200, 200, 200); } 444 } 445 #outer { 446 color: rgb(100, 100, 100); 447 } 448 #div { 449 animation: test 100s -50s linear paused; 450 } 451 `, (style) => { 452 // See https://github.com/w3c/csswg-drafts/issues/10371 for the two 453 // possibilities. 454 assert_in_array(getComputedStyle(div).getPropertyValue(name), [ 455 'color-mix(in srgb, currentcolor, rgb(200, 200, 200))', 456 'rgb(150, 150, 150)', 457 ]); 458 459 outer.style.color = 'rgb(50, 50, 50)'; 460 assert_in_array(getComputedStyle(div).getPropertyValue(name), [ 461 'color-mix(in srgb, currentcolor, rgb(200, 200, 200))', 462 'rgb(125, 125, 125)', 463 ]); 464 465 outer.style.color = 'rgb(150, 150, 150)'; 466 assert_in_array(getComputedStyle(div).getPropertyValue(name), [ 467 'color-mix(in srgb, currentcolor, rgb(200, 200, 200))', 468 'rgb(175, 175, 175)', 469 ]); 470 471 outer.style.removeProperty("color"); 472 }); 473 }, 'CSS animation setting "currentColor" for a custom property on a keyframe is responsive to changing "color" on the parent.'); 474 475 test_with_at_property({ 476 syntax: '"<color>"', 477 inherits: false, 478 initialValue: 'black' 479 }, (name) => { 480 with_style_node(` 481 #outer { 482 color: rgb(100, 100, 100); 483 } 484 `, () => { 485 const animation = div.animate({ [name]: ["currentcolor", "rgb(200, 200, 200)"]}, 1000); 486 animation.currentTime = 500; 487 animation.pause(); 488 489 assert_in_array(getComputedStyle(div).getPropertyValue(name), [ 490 'color-mix(in srgb, currentcolor, rgb(200, 200, 200))', 491 'rgb(150, 150, 150)', 492 ]); 493 494 outer.style.color = 'rgb(50, 50, 50)'; 495 assert_in_array(getComputedStyle(div).getPropertyValue(name), [ 496 'color-mix(in srgb, currentcolor, rgb(200, 200, 200))', 497 'rgb(125, 125, 125)', 498 ]); 499 500 outer.style.color = 'rgb(150, 150, 150)'; 501 assert_in_array(getComputedStyle(div).getPropertyValue(name), [ 502 'color-mix(in srgb, currentcolor, rgb(200, 200, 200))', 503 'rgb(175, 175, 175)', 504 ]); 505 506 outer.style.removeProperty("color"); 507 }); 508 }, 'JS-originated animation setting "currentColor" for a custom property on a keyframe is responsive to changing "color" on the parent.'); 509 510 </script>