[scroll-animations] Implement insets in ViewTimeline (minus auto)

This CL implements the functionality behind view-timeline-inset, except
that 'auto' just returns zero for now. It will be implemented in a future
CL.

Bug: 1344151
Change-Id: I500ba8724df729ecfcb2edaabfc60fba73ad2da6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3891062
Reviewed-by: Kevin Ellis <kevers@chromium.org>
Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1048023}
diff --git a/scroll-animations/css/view-timeline-inset-animation.html b/scroll-animations/css/view-timeline-inset-animation.html
new file mode 100644
index 0000000..82bfb93
--- /dev/null
+++ b/scroll-animations/css/view-timeline-inset-animation.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<title>Animations using view-timeline-inset</title>
+<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#propdef-view-timeline-inset">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/web-animations/testcommon.js"></script>
+<style>
+  @keyframes anim {
+    from { z-index: 0; }
+    to { z-index: 100; }
+  }
+  #scroller {
+    overflow: hidden;
+    width: 80px;
+    height: 100px;
+  }
+  #target {
+    margin: 150px;
+    width: 50px;
+    height: 50px;
+    z-index: -1;
+  }
+</style>
+<main id=main></main>
+<script>
+  function inflate(t, template) {
+    t.add_cleanup(() => main.replaceChildren());
+    main.append(template.content.cloneNode(true));
+  }
+  async function scrollTop(e, value) {
+    e.scrollTop = value;
+    await waitForNextFrame();
+  }
+  async function scrollLeft(e, value) {
+    e.scrollLeft = value;
+    await waitForNextFrame();
+  }
+  async function assertValueAt(scroller, target, args) {
+    if (args.scrollTop !== undefined)
+      await scrollTop(scroller, args.scrollTop);
+    if (args.scrollLeft !== undefined)
+      await scrollLeft(scroller, args.scrollLeft);
+    assert_equals(getComputedStyle(target).zIndex, args.expected.toString());
+  }
+</script>
+
+<!--
+  The scroller is 80x100px.
+  The target is 50x50px with a 150px margin.
+-->
+
+<template id=test_one_value>
+  <style>
+    #target {
+      view-timeline: t1;
+      view-timeline-inset: 10px;
+      animation: anim 1s linear t1;
+    }
+  </style>
+  <div id=scroller class=vertical>
+    <div id=target></div>
+  </div>
+</template>
+<script>
+  promise_test(async (t) => {
+    inflate(t, test_one_value);
+    await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+    await assertValueAt(scroller, target, { scrollTop:50 + 10, expected:0 }); // 0%
+    await assertValueAt(scroller, target, { scrollTop:125, expected:50 }); // 50%
+    await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+    await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+  }, 'view-timeline-inset with one value');
+</script>
+
+<template id=test_two_values>
+  <style>
+    #target {
+      view-timeline: t1;
+      view-timeline-inset: 10px 20px;
+      animation: anim 1s linear t1;
+    }
+  </style>
+  <div id=scroller class=vertical>
+    <div id=target></div>
+  </div>
+</template>
+<script>
+  promise_test(async (t) => {
+    inflate(t, test_two_values);
+    await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+    await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+    await assertValueAt(scroller, target, { scrollTop:130, expected:50 }); // 50%
+    await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+    await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+  }, 'view-timeline-inset with two values');
+</script>
+
+<template id=test_em_values>
+  <style>
+    #target {
+      font-size: 10px;
+      view-timeline: t1;
+      view-timeline-inset: 10px 2em;
+      animation: anim 1s linear t1;
+    }
+  </style>
+  <div id=scroller class=vertical>
+    <div id=target></div>
+  </div>
+</template>
+<script>
+  promise_test(async (t) => {
+    inflate(t, test_em_values);
+    await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+    await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+    await assertValueAt(scroller, target, { scrollTop:130, expected:50 }); // 50%
+    await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+    await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+  }, 'view-timeline-inset with em values');
+</script>
+
+<template id=test_percentage_values>
+  <style>
+    #target {
+      font-size: 10px;
+      view-timeline: t1;
+      view-timeline-inset: calc(5px + max(1%, 5%)) 20%;
+      animation: anim 1s linear t1;
+    }
+  </style>
+  <div id=scroller class=vertical>
+    <div id=target></div>
+  </div>
+</template>
+<script>
+  promise_test(async (t) => {
+    inflate(t, test_percentage_values);
+    await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+    await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+    await assertValueAt(scroller, target, { scrollTop:130, expected:50 }); // 50%
+    await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+    await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+  }, 'view-timeline-inset with percentage values');
+</script>
+
+<template id=test_outset>
+  <style>
+    #target {
+      view-timeline: t1;
+      view-timeline-inset: -10px -20px;
+      animation: anim 1s linear t1;
+    }
+  </style>
+  <div id=scroller class=vertical>
+    <div id=target></div>
+  </div>
+</template>
+<script>
+  promise_test(async (t) => {
+    inflate(t, test_outset);
+    await assertValueAt(scroller, target, { scrollTop:20, expected:-1 });
+    await assertValueAt(scroller, target, { scrollTop:50 - 20, expected:0 }); // 0%
+    await assertValueAt(scroller, target, { scrollTop:120, expected:50 }); // 50%
+    await assertValueAt(scroller, target, { scrollTop:200 + 10, expected:100 }); // 100%
+    await assertValueAt(scroller, target, { scrollTop:220, expected:-1 });
+  }, 'view-timeline-inset with negative values');
+</script>
+
+<template id=test_horizontal>
+  <style>
+    #target {
+      view-timeline: t1 horizontal;
+      view-timeline-inset: 10px 20px;
+      animation: anim 1s linear t1;
+    }
+  </style>
+  <div id=scroller>
+    <div id=target></div>
+  </div>
+</template>
+<script>
+  promise_test(async (t) => {
+    inflate(t, test_horizontal);
+    await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 });
+    await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0%
+    await assertValueAt(scroller, target, { scrollLeft:140, expected:50 }); // 50%
+    await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100%
+    await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 });
+  }, 'view-timeline-inset with horizontal scroller');
+</script>
+
+<template id=test_block>
+  <style>
+    #target {
+      view-timeline: t1 block;
+      view-timeline-inset: 10px 20px;
+      animation: anim 1s linear t1;
+    }
+  </style>
+  <div id=scroller>
+    <div id=target></div>
+  </div>
+</template>
+<script>
+  promise_test(async (t) => {
+    inflate(t, test_block);
+    await assertValueAt(scroller, target, { scrollTop:50, expected:-1 });
+    await assertValueAt(scroller, target, { scrollTop:50 + 20, expected:0 }); // 0%
+    await assertValueAt(scroller, target, { scrollTop:130, expected:50 }); // 50%
+    await assertValueAt(scroller, target, { scrollTop:200 - 10, expected:100 }); // 100%
+    await assertValueAt(scroller, target, { scrollTop:200, expected:-1 });
+  }, 'view-timeline-inset with block scroller');
+</script>
+
+<template id=test_inline>
+  <style>
+    #target {
+      view-timeline: t1 inline;
+      view-timeline-inset: 10px 20px;
+      animation: anim 1s linear t1;
+    }
+  </style>
+  <div id=scroller>
+    <div id=target></div>
+  </div>
+</template>
+<script>
+  promise_test(async (t) => {
+    inflate(t, test_inline);
+    await assertValueAt(scroller, target, { scrollLeft:20, expected:-1 });
+    await assertValueAt(scroller, target, { scrollLeft:70 + 20, expected:0 }); // 0%
+    await assertValueAt(scroller, target, { scrollLeft:140, expected:50 }); // 50%
+    await assertValueAt(scroller, target, { scrollLeft:200 - 10, expected:100 }); // 100%
+    await assertValueAt(scroller, target, { scrollLeft:200, expected:-1 });
+  }, 'view-timeline-inset with inline scroller');
+</script>
diff --git a/scroll-animations/css/view-timeline-inset-computed.html b/scroll-animations/css/view-timeline-inset-computed.html
index 9cc272a..77683a4 100644
--- a/scroll-animations/css/view-timeline-inset-computed.html
+++ b/scroll-animations/css/view-timeline-inset-computed.html
@@ -18,6 +18,8 @@
 test_computed_value('view-timeline-inset', 'unset', '0px');
 test_computed_value('view-timeline-inset', 'revert', '0px');
 test_computed_value('view-timeline-inset', '1px');
+test_computed_value('view-timeline-inset', '1%');
+test_computed_value('view-timeline-inset', 'calc(1% + 1px)');
 test_computed_value('view-timeline-inset', '1px 2px');
 test_computed_value('view-timeline-inset', '1px 2em', '1px 20px');
 test_computed_value('view-timeline-inset', 'calc(1px + 1em) 2px', '11px 2px');
diff --git a/scroll-animations/css/view-timeline-used-values.html b/scroll-animations/css/view-timeline-used-values.html
index 883c062..cf32777 100644
--- a/scroll-animations/css/view-timeline-used-values.html
+++ b/scroll-animations/css/view-timeline-used-values.html
@@ -68,3 +68,32 @@
     assert_equals(getComputedStyle(target).zIndex, '35');
   }, 'Use the last value from view-timeline-axis if omitted');
 </script>
+
+<template id=omitted_inset>
+  <style>
+    #target {
+      view-timeline-name: t1, t2; /* Two items */
+      view-timeline-inset: 100px; /* One item */
+      animation: anim 1s linear t2;
+    }
+  </style>
+  <div id=scroller class=scroller>
+    <div id=target></div>
+  </div>
+</template>
+<script>
+  promise_test(async (t) => {
+    inflate(t, omitted_inset);
+    assert_equals(getComputedStyle(target).zIndex, '-1');
+
+    // 0% is normally at at scrollTop = -100
+    // 100% is normally at scrollTop/Left = 300
+    // However, we have a 100px inset in both ends, which makes the
+    // range [0, 200].
+
+    await scrollTop(scroller, 0);
+    assert_equals(getComputedStyle(target).zIndex, '0');
+    await scrollTop(scroller, 100); // 50%
+    assert_equals(getComputedStyle(target).zIndex, '50');
+  }, 'Use the last value from view-timeline-inset if omitted');
+</script>