timeline-offset-in-keyframe.html (9086B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#named-timeline-range"> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 <script src="/web-animations/testcommon.js"></script> 9 <script src="support/testcommon.js"></script> 10 <title>Animation range and delay</title> 11 </head> 12 <style type="text/css"> 13 #scroller { 14 border: 10px solid lightgray; 15 overflow-y: scroll; 16 overflow-x: hidden; 17 width: 300px; 18 height: 200px; 19 } 20 #target { 21 margin: 800px 10px; 22 width: 100px; 23 height: 100px; 24 z-index: -1; 25 background-color: green; 26 } 27 </style> 28 <body> 29 <div id=scroller> 30 <div id=target></div> 31 </div> 32 </body> 33 <script type="text/javascript"> 34 async function runTest() { 35 function assert_progress_equals(anim, expected, errorMessage) { 36 assert_approx_equals( 37 anim.effect.getComputedTiming().progress, 38 expected, 1e-6, errorMessage); 39 } 40 41 function assert_opacity_equals(expected, errorMessage) { 42 assert_approx_equals( 43 parseFloat(getComputedStyle(target).opacity), expected, 1e-6, 44 errorMessage); 45 } 46 47 async function runTimelineOffsetsInKeyframesTest(keyframes) { 48 const testcase = JSON.stringify(keyframes); 49 const anim = target.animate(keyframes, { 50 timeline: new ViewTimeline( { subject: target }), 51 rangeStart: { rangeName: 'contain', offset: CSS.percent(0) }, 52 rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, 53 duration: 'auto', fill: 'both' 54 }); 55 await anim.ready; 56 await waitForNextFrame(); 57 58 // @ contain 0% 59 scroller.scrollTop = 700; 60 await waitForNextFrame(); 61 62 assert_progress_equals( 63 anim, 0, `Testcase '${testcase}': progress at contain 0%`); 64 assert_opacity_equals( 65 1/3, `Testcase '${testcase}': opacity at contain 0%`); 66 67 // @ contain 50% 68 scroller.scrollTop = 750; 69 await waitForNextFrame(); 70 assert_progress_equals( 71 anim, 0.5, `Testcase '${testcase}': progress at contain 50%`); 72 assert_opacity_equals( 73 0.5, `Testcase '${testcase}': opacity at contain 50%`); 74 75 // @ contain 100% 76 scroller.scrollTop = 800; 77 await waitForNextFrame(); 78 assert_progress_equals( 79 anim, 1, `Testcase '${testcase}': progress at contain 100%`); 80 assert_opacity_equals( 81 2/3, `Testcase '${testcase}': opacity at contain 100%`); 82 anim.cancel(); 83 } 84 85 async function runParseNumberOrPercentInKeyframesTest(keyframes) { 86 const anim = target.animate(keyframes, { 87 timeline: new ViewTimeline( { subject: target }), 88 rangeStart: { rangeName: 'contain', offset: CSS.percent(0) }, 89 rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, 90 duration: 'auto', fill: 'both' 91 }); 92 await anim.ready; 93 await waitForNextFrame(); 94 95 const maxScroll = scroller.scrollHeight - scroller.clientHeight; 96 scroller.scrollTop = maxScroll / 2; 97 await waitForNextFrame(); 98 99 const testcase = JSON.stringify(keyframes); 100 assert_progress_equals(anim, 0.5, testcase); 101 assert_opacity_equals(0.5, testcase); 102 anim.cancel(); 103 } 104 105 async function runInvalidKeyframesTest(keyframes) { 106 assert_throws_js(TypeError, () => { 107 target.animate(keyframes, { 108 timeline: new ViewTimeline( { subject: target }), 109 }); 110 }, `Invalid keyframes test case "${JSON.stringify(keyframes)}"`); 111 } 112 113 promise_test(async t => { 114 // Test equivalent typed-OM and CSS representations of timeline offsets. 115 // Test array and object form for keyframes. 116 const keyframeTests = [ 117 // BaseKeyframe form with offsets expressed as typed-OM. 118 [ 119 { 120 offset: { rangeName: 'cover', offset: CSS.percent(0) }, 121 opacity: 0 122 }, 123 { 124 offset: { rangeName: 'cover', offset: CSS.percent(100) }, 125 opacity: 1 126 } 127 ], 128 // BaseKeyframe form with offsets expressed as CSS text. 129 [ 130 { offset: "cover 0%", opacity: 0 }, 131 { offset: "cover 100%", opacity: 1 } 132 ], 133 // BasePropertyIndexedKeyframe form with offsets expressed as typed-OM. 134 { 135 opacity: [0, 1], 136 offset: [ 137 { rangeName: 'cover', offset: CSS.percent(0) }, 138 { rangeName: 'cover', offset: CSS.percent(100) } 139 ] 140 }, 141 // BasePropertyIndexedKeyframe form with offsets expressed as CSS text. 142 { opacity: [0, 1], offset: [ "cover 0%", "cover 100%" ]} 143 ]; 144 145 for (let i = 0; i < keyframeTests.length; i++) { 146 await runTimelineOffsetsInKeyframesTest(keyframeTests[i]); 147 } 148 149 }, 'Timeline offsets in programmatic keyframes'); 150 151 promise_test(async t => { 152 const keyframeTests = [ 153 [{offset: "0.5", opacity: 0.5 }], 154 [{offset: "50%", opacity: 0.5 }], 155 [{offset: "calc(20% + 30%)", opacity: 0.5 }] 156 ]; 157 158 for (let i = 0; i < keyframeTests.length; i++) { 159 await runParseNumberOrPercentInKeyframesTest(keyframeTests[i]); 160 } 161 162 }, 'String offsets in programmatic keyframes'); 163 164 promise_test(async t => { 165 const invalidKeyframeTests = [ 166 // BasePropertyKefyrame: 167 [{ offset: { rangeName: 'somewhere', offset: CSS.percent(0) }}], 168 [{ offset: { rangeName: 'entry', offset: CSS.px(0) }}], 169 [{ offset: "here 0%" }], 170 [{ offset: "entry 3px" }], 171 // BasePropertyIndexedKeyframe with sequence: 172 { offset: [{ rangeName: 'somewhere', offset: CSS.percent(0) }]}, 173 { offset: [{ rangeName: 'entry', offset: CSS.px(0) }]}, 174 { offset: ["here 0%"] }, 175 { offset: ["entry 3px" ]}, 176 // BasePropertyIndexedKeyframe without sequence: 177 { offset: { rangeName: 'somewhere', offset: CSS.percent(0) }}, 178 { offset: { rangeName: 'entry', offset: CSS.px(0) }}, 179 { offset: "here 0%" }, 180 { offset: "entry 3px" }, 181 // <number> or <percent> as string: 182 [{ offset: "-1" }], 183 [{ offset: "2" }], 184 [{ offset: "-10%" }], 185 [{ offset: "110%" }], 186 { offset: ["-1"], opacity: [0.5] }, 187 { offset: ["2"], opacity: [0.5] }, 188 { offset: "-1", opacity: 0.5 }, 189 { offset: "2", opacity: 0.5 }, 190 // Extra stuff at the end. 191 [{ offset: "0.5 trailing nonsense" }], 192 [{ offset: "cover 50% eureka" }] 193 ]; 194 for( let i = 0; i < invalidKeyframeTests.length; i++) { 195 await runInvalidKeyframesTest(invalidKeyframeTests[i]); 196 } 197 }, 'Invalid timeline offset in programmatic keyframe throws'); 198 199 200 promise_test(async t => { 201 const anim = target.animate([ 202 { offset: "cover 0%", opacity: 0 }, 203 { offset: "cover 100%", opacity: 1 } 204 ], { 205 rangeStart: { rangeName: 'contain', offset: CSS.percent(0) }, 206 rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, 207 duration: 10000, fill: 'both' 208 }); 209 210 scroller.scrollTop = 750; 211 212 await anim.ready; 213 assert_opacity_equals(1, `Opacity with document timeline`); 214 215 anim.timeline = new ViewTimeline( { subject: target }); 216 await anim.ready; 217 218 assert_progress_equals(anim, 0.5, `Progress at contain 50%`); 219 assert_opacity_equals(0.5, `Opacity at contain 50%`); 220 221 anim.timeline = document.timeline; 222 assert_false(anim.pending); 223 await waitForNextFrame(); 224 assert_opacity_equals(1, `Opacity after resetting timeline`); 225 226 anim.cancel(); 227 }, 'Timeline offsets in programmatic keyframes adjust for change in ' + 228 'timeline'); 229 230 promise_test(async t => { 231 const anim = target.animate([], { 232 timeline: new ViewTimeline( { subject: target }), 233 rangeStart: { rangeName: 'contain', offset: CSS.percent(0) }, 234 rangeEnd: { rangeName: 'contain', offset: CSS.percent(100) }, 235 duration: 'auto', fill: 'both' 236 }); 237 238 await anim.ready; 239 await waitForNextFrame(); 240 241 scroller.scrollTop = 750; 242 await waitForNextFrame(); 243 assert_progress_equals( 244 anim, 0.5, `Progress at contain 50% before effect change`); 245 assert_opacity_equals(1, `Opacity at contain 50% before effect change`); 246 247 anim.effect = new KeyframeEffect(target, [ 248 { offset: "cover 0%", opacity: 0 }, 249 { offset: "cover 100%", opacity: 1 } 250 ], { duration: 'auto', fill: 'both' }); 251 await waitForNextFrame(); 252 assert_progress_equals( 253 anim, 0.5, `Progress at contain 50% after effect change`); 254 assert_opacity_equals(0.5, `Opacity at contain 50% after effect change`); 255 }, 'Timeline offsets in programmatic keyframes resolved when updating ' + 256 'the animation effect'); 257 } 258 259 // TODO(kevers): Add tests for getKeyframes once 260 // https://github.com/w3c/csswg-drafts/issues/8507 is resolved. 261 262 window.onload = runTest; 263 </script> 264 </html>