LEVEL 1 · พื้นฐานที่ใช้ทุกวัน
Timeline เดียวที่หยุดนิ่ง
บทที่ 2 ทิ้งไว้ว่า timeline ต้อง "สร้างจบแล้วหยุดนิ่งรอ" บทนี้ลงมือจริงด้วย GSAP และไขว่าทำไมท่าเขียนบางท่าปลอดภัยกว่าอีกท่าทั้งที่ผลลัพธ์บนตาเหมือนกัน
5.1 สัญญาสามข้อ
ทุก composition ลงทะเบียน timeline เดียว แบบนี้ (โค้ดจริงจากการ์ด):
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.fromTo(
"#headline-text",
{ autoAlpha: 0, y: 40 },
{ autoAlpha: 1, y: 0, duration: 0.8, ease: "power3.out" },
0.6
);
window.__timelines["main"] = tl;สัญญา: (ก) paused: true — คนหมุนเวลาคือเครื่องเรนเดอร์ (ข) สร้างแบบ synchronous ตอนโหลดเพจ ห้ามไปอยู่ใน setTimeout / Promise / event handler เพราะเครื่องอาจถามเฟรมก่อนโค้ดพวกนั้นวิ่ง (ค) กุญแจ "main" ต้องตรงกับ data-composition-id ของ root เป๊ะ — มันคือช่องทางเดียวที่เครื่องใช้ตามหา timeline
ตัวเลขท้ายบรรทัด (0.6) คือตำแหน่งบนแกนเวลาของเรื่อง — คุณกำลังวางแผนอนาคต ไม่ใช่สั่งเล่น
paused: true) — เข็มไม่เดินเอง เครื่องเรนเดอร์เป็นคนหมุนไปยัง t ที่ต้องการ · กุญแจ window.__timelines ต้องตรงกับ data-composition-id เป๊ะ เพราะเป็นช่องทางเดียวที่เครื่องใช้ตามหา timeline5.2 ทำไมต้อง fromTo — กลไกที่มองไม่เห็นด้วยตา
gsap.to() บอกแค่ปลายทาง แล้วให้ GSAP "จำสภาพตอนเริ่ม" เอาเอง ปัญหาคือมันจำตอนที่ถูกถามถึงครั้งแรก — และเครื่องเรนเดอร์ถามเฟรมไม่เรียงลำดับ (หลาย worker แบ่งกันถาม — บทที่ 11) สภาพตอนเริ่มที่ถูกจำจึงขึ้นกับลำดับการถาม ผิดกฎ f(t) ทันที
gsap.fromTo() ประกาศทั้งสองปลายเอง ไม่ต้องจำอะไร ถามลำดับไหนก็ได้คำตอบเดิม ผมทดลองเพิ่ม CSS transform: translateY(40px) ให้ element เดียวกับที่มี fromTo แล้วถ่ายเฟรมก่อนคิวเข้า: element หายไปเลย — fromTo ยึดสภาพเริ่มของมัน (ซ่อนไว้) ตั้งแต่เพจโหลด ค่า CSS ไม่มีผลอะไร นี่คือเหตุผลที่กติกาบอกให้ตั้งค่าเริ่มใน tween ไม่ใช่ใน CSS: ให้ความจริงมีที่อยู่ที่เดียว
5.3 ลืมลงทะเบียนแล้วใครจับ — ทดลองจริง
ผมคอมเมนต์บรรทัด window.__timelines["main"] = tl; ทิ้ง: lint เงียบสนิท แต่ check ล้มด้วยสามข้อจาก motion sidecar — ไฟล์ index.motion.json ที่ผมประกาศเจตนาไว้ข้าง composition:
✗ motion_appears_late: never reaches visible opacity but should be visible by 1.6s
Fix: The renderer seeks a paused timeline; a forward-only reveal can be skipped.
✗ motion_out_of_order: #headline-text should appear before #outro-card-line ...
✗ motion_frozen: nothing moves within #root between 2.5s and 6.5s (4s static)ไม่มี timeline = ไม่มีอะไรขยับ และเครื่องรู้เพราะผมบอกไว้ก่อนว่าอะไรควรขยับเมื่อไหร่ sidecar หน้าตาแบบนี้:
{
"assertions": [
{ "kind": "appearsBy", "selector": "#headline-text", "bySec": 1.6 },
{ "kind": "before", "a": "#headline-text", "b": "#outro-card-line" },
{ "kind": "keepsMoving", "withinSelector": "#root" }
]
}ประกาศเจตนาให้เครื่องอ่านได้ แล้วเครื่องจะเฝ้าแทนคุณ — นี่คือหัวใจของการทำวิดีโอแบบวิศวกร