diff --git a/docs/specs/composition.md b/docs/specs/composition.md
index 619ec3aa..f5b52f48 100644
--- a/docs/specs/composition.md
+++ b/docs/specs/composition.md
@@ -6,6 +6,21 @@
{schema_object:composition/animation}
+### Time and Duration
+
+The animation's playable frame range is defined by `ip` (In Point, inclusive)
+and `op` (Out Point, exclusive). The frame at `op` is NOT rendered; the
+animation loops or stops at that boundary.
+
+The total duration in frames is `op - ip`, and the duration in seconds is
+`(op - ip) / fr`.
+
+Implementations MUST treat `op` as an exclusive boundary. `op` MUST be
+strictly greater than `ip`.
+
+Example: `ip: 0`, `op: 30`, `fr: 30` defines a 30-frame, 1-second animation
+playing frames `0..29`.
+
### Versioning Guidelines
The Lottie specification version number uses a semantic versioning system,
diff --git a/docs/specs/layers.md b/docs/specs/layers.md
index adbaa8dc..db2bf8d3 100644
--- a/docs/specs/layers.md
+++ b/docs/specs/layers.md
@@ -12,6 +12,15 @@ The `ty` property defines the specific layer type based on the following values:
{schema_subtype_table:layers/all-layers:ty}
+#### Visibility Range
+
+A layer is visible on frame `t` when `ip <= t < op`. The `ip` (In Point) boundary is
+inclusive, and the `op` (Out Point) boundary is exclusive — the frame at `op` is NOT
+rendered. This matches the Animation-level [`ip`/`op`](composition.md#animation)
+semantics.
+
+`op` MUST NOT be less than `ip`.
+
Visual Layer
{schema_string:layers/visual-layer/description}
@@ -143,10 +152,10 @@ The `tm` property specifies a time remap function as an animatable property, all
over the precomp timeline (subset, speedup/slowdown, reverse, frame-freeze, or any other arbitrary
transformation).
-It maps the current layer time (in the frame index $[ip \ldots op]$ domain) to a precomp time
+It maps the current layer time (in the half-open frame index $[ip, op)$ domain) to a precomp time
expressed in seconds, and evaluates all animatable precomp properties based on the new time value:
-$$tm \colon \left[ip \ldots op\right] \mapsto seconds$$
+$$tm \colon \left[ip, op\right) \mapsto seconds$$
$$t\prime = tm(t) \cdot FPS$$
Note: the global frame rate factor $FPS$ ([Animation](composition.md#Animation) `fr` property) is
diff --git a/schema/composition/animation.json b/schema/composition/animation.json
index b6df275e..776dc274 100644
--- a/schema/composition/animation.json
+++ b/schema/composition/animation.json
@@ -24,13 +24,14 @@
},
"ip": {
"title": "In Point",
- "description": "Frame the animation starts at (usually 0)",
+ "description": "Frame the animation starts at (inclusive; usually 0)",
"type": "number"
},
"op": {
"title": "Out Point",
- "description": "Frame the animation stops/loops at, which makes this the duration in frames when `ip` is 0",
- "type": "number"
+ "description": "Frame the animation stops/loops at (exclusive). The frame at `op` is NOT rendered; the animation duration in frames is `op - ip`.",
+ "type": "number",
+ "exclusiveMinimum": 0
},
"w": {
"title": "Width",
diff --git a/schema/layers/layer.json b/schema/layers/layer.json
index 697a91cb..2c259333 100644
--- a/schema/layers/layer.json
+++ b/schema/layers/layer.json
@@ -32,12 +32,12 @@
},
"ip": {
"title": "In Point",
- "description": "Frame when the layer becomes visible",
+ "description": "Frame when the layer becomes visible (inclusive).",
"type": "number"
},
"op": {
"title": "Out Point",
- "description": "Frame when the layer becomes invisible",
+ "description": "Frame when the layer becomes invisible (exclusive). The layer is visible while `ip <= t < op`.",
"type": "number"
}
},