7 คะแนน โดย GN⁺ 2024-05-06 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เมื่อ CSS รองรับฟังก์ชันคณิตศาสตร์มากขึ้น แนวทางการคำนวณสถานะแอนิเมชันด้วย ตัวแปร tick เวลา --t แทน keyframes จึงกลับมาใช้งานได้จริงอีกครั้ง
  • เมื่อนำ @property --t มารวมกับแอนิเมชันที่ระดับรากซึ่งมีความยาว 86400000ms จะทำให้ค่าเวลาเพิ่มขึ้นในระดับมิลลิวินาที และสามารถเชื่อมค่านี้เข้ากับการคำนวณพร็อพเพอร์ตีอย่าง rotate, translate ได้
  • หากใช้ step() ก็สามารถจำกัด เฟรมเรต ให้เป็น 8fps, 24fps, 60fps ได้ และด้วยการปรับแต่งประสิทธิภาพของเบราว์เซอร์ ความถี่การอัปเดตที่สูงกว่า 60fps ก็มักไม่เป็นปัญหา
  • ใช้ min(), mod(), sin(), pow(), round() เพื่อจำกัด·ทำซ้ำ·ทำให้แกว่ง·ใส่ easing ให้กับค่าเวลา และแสดง custom easing กับการย้ายตำแหน่งได้โดยไม่ต้องใช้ keyframes
  • css-doodle มีรูปแบบย่ออย่าง @t, @T, @TS ช่วยให้เดโมที่มีสมการจำนวนมากเขียนได้สั้นลงและทดลองได้รวดเร็วขึ้น

สร้าง time tick เป็นตัวแปร CSS

  • แนวทางในอดีตมีขอบเขตการใช้งานจำกัด เพราะใน CSS ทำ การคำนวณทางคณิตศาสตร์ ที่ซับซ้อนได้ยาก
  • ปัจจุบัน CSS รองรับฟังก์ชันคณิตศาสตร์ที่จำเป็น เช่น mod(), round() และฟังก์ชันตรีโกณมิติ ทำให้สามารถลองทำแอนิเมชันแบบอิงเวลาได้อีกครั้ง
  • การดูเดโมอาจต้องเปิด Experimental feature flags

หลักการทำงานพื้นฐาน

  • CSS ไม่สามารถเริ่มตัวจับเวลาได้โดยตรงแบบ JavaScript แต่สามารถใช้ CSS Houdini API เพื่อกำหนดตัวแปรคัสตอม --t ให้ติดตามเวลาในหน่วยมิลลิวินาทีได้
  • ตัวอย่างพื้นฐานประกาศ @property --t เป็น <integer> และใน @keyframes tick ให้เปลี่ยนค่าจาก 0 ถึง 86400000 จากนั้นใช้แอนิเมชัน 86400000ms linear infinite กับ :root
  • --t จะเพิ่มขึ้นทีละ 1 ทุก 1 มิลลิวินาที และจะเป็น 1000 ในเวลา 1 วินาที
  • หากใช้ counter() ก็สามารถแสดงค่า --t ปัจจุบันบนหน้าจอได้
  • ถ้าเชื่อมการคำนวณที่อิง --t เข้ากับพร็อพเพอร์ตี เช่น rotate: calc(var(--t) * .001turn) ค่าก็จะเปลี่ยนไปตามเวลาและกลายเป็น แอนิเมชัน

การควบคุมเฟรมเรต

  • สำหรับแอนิเมชันที่ลื่นไหล ความถี่การอัปเดตที่ 60fps ก็เพียงพอ
  • ด้วยการปรับแต่งการเรนเดอร์ของเบราว์เซอร์ แม้ความถี่การอัปเดตจะสูงกว่า 60fps ก็มักไม่เป็นปัญหา
  • หากจำเป็น สามารถใช้ step() ใน animation-timing-function เพื่อจำกัดเฟรมเรตโดยตรงได้
    • 8fps: step(calc(86400000/(1000/8)))
    • 24fps: step(calc(86400000/(1000/24)))
    • 60fps: step(calc(86400000/(1000/60)))

เปลี่ยนค่าเวลาที่เพิ่มขึ้นให้เป็นค่าแอนิเมชัน

  • ค่า --t จะเพิ่มขึ้นต่อเนื่องไปในทิศทางเดียว
  • ค่า angle แม้จะเกิน 360deg ก็ไม่มีปัญหา แต่ไม่ใช่ทุกพร็อพเพอร์ตีของ CSS ที่จัดการค่าแบบ วนรอบ
  • translate: calc(var(--t) * .001px) จะทำให้กล่องเลื่อนไปทางขวาเรื่อย ๆ
  • หยุดด้วย min()

    • หากต้องการให้หยุดทันทีเมื่อถึง offset ที่กำหนด สามารถใช้ min() ได้
    • translate: min(270px, calc(var(--t) * .5px)) จะจำกัดค่าการเคลื่อนที่ไว้ที่ 270px
    • หากต้องการควบคุมระยะเวลาให้แม่นยำ สามารถจำกัด --t เองได้
    • translate: calc(min(3000, var(--t)) * (270px / 3000))
    • การคำนวณนี้ทำให้เคลื่อนที่ 270px ภายใน 3 วินาที
  • ทำซ้ำด้วย mod()

    • หากต้องการให้กล่องเลื่อนไปทางขวาแล้วกลับไปเริ่มใหม่จากต้น สามารถใช้ mod() ได้
    • translate: calc(mod(var(--t)/4, 270) * 1px) ใช้ทำให้ offset วนซ้ำได้
  • สร้างการเคลื่อนที่ไป-กลับด้วย sin()

    • การเคลื่อนที่ไป-กลับสามารถแสดงด้วย sin()
    • เชื่อมค่าเวลาเข้ากับฟังก์ชันตรีโกณมิติ เช่น translate: calc(sin(mod(var(--t)/135, 270)) * 135px)

ฟังก์ชัน easing แบบกำหนดเอง

  • ด้วยฟังก์ชันคณิตศาสตร์ของ CSS และตัวแปร --t จึงสามารถสร้าง custom easing ที่อาจทำได้ยากด้วย cubic-bezier()
  • ก่อนอื่นให้จำกัด --t ให้อยู่ในช่วงระหว่าง 0 และ 1
    • --t01: calc(min(1000, var(--t)) / 1000) จะเปลี่ยนจาก 0 เป็น 1 ภายใน 1 วินาที
  • ease-out-cubic

    • ease-out-cubic ประกอบด้วยสมการ 1 - pow(1 - t, 3)
    • สร้างค่า --ease-out-cubic แล้วเชื่อมเข้ากับ translate: calc(var(--ease-out-cubic) * 270px)
  • ease-out-elastic

    • ease-out-elastic ประกอบด้วยสมการ pow(2, -10t) * sin((10t - .75) * 2/3 * PI) + 1
    • ใช้วิธีเดียวกันสร้างค่า --ease-out-elastic แล้วคูณกับ translate เพื่อนำไปใช้กับการเคลื่อนที่ 270px

ย่อสมการด้วย css-doodle

  • เมื่อสมการซับซ้อนขึ้น var() และ calc() จะทำให้อ่านโค้ดยากขึ้น
  • เวอร์ชันล่าสุดของ css-doodle เพิ่ม ฟังก์ชัน @t สำหรับแทน --t และสามารถรับสมการคณิตศาสตร์ง่าย ๆ ภายในอาร์กิวเมนต์ได้โดยตรง
  • รูปแบบ rotate สามารถย่อได้ดังนี้
    • เดิม: rotate: calc(mod(var(--t) / 1000, 10) * 5deg)
    • css-doodle: rotate: @t(/1000, %10, *5deg)
  • สามารถสร้างแอนิเมชันแบบอิงคณิตศาสตร์ด้วยโค้ดสั้น ๆ ได้โดยไม่ต้องเขียน keyframes
  • เมื่อเปลี่ยนพารามิเตอร์ ก็สามารถทดลองผลลัพธ์ใหม่ ๆ ได้อย่างรวดเร็ว
  • @T และ @TS

    • นอกจาก @t แล้ว ตัวพิมพ์ใหญ่ @T หมายถึง time ticks นับจากจุดเริ่มต้นของวัน
    • @TS เป็น shorthand ของ @t(/1000) และใช้ติดตามเวลาเป็นหน่วยวินาที
    • ตัวอย่างนาฬิกาดูได้ที่ CodePen
    • เข็มวินาที: rotate: @TS(*6, %360deg)
    • เข็มนาที: rotate: @TS(/60, *6, %360deg)
    • เข็มชั่วโมง: rotate: @TS(/60, /12, *6, %360deg)
  • สร้างการกระโดดของเข็มวินาทีด้วย round()

    • หากต้องการให้เข็มวินาทีขยับแบบกระโดดเป็นจังหวะ สามารถใช้ round() ได้
    • พารามิเตอร์ตัวที่สามของ round() ใช้กำหนดช่วงการปัดค่า
    • สำหรับนาฬิกา หนึ่งขั้นคือ 360 / 60 = 6deg ดังนั้นจึงใช้ rotate: round(down, @TS(*6, %360deg), 6deg)

เดโมและจุดที่นำไปใช้

  • ตัวอย่างการทำแอนิเมชันสีและตำแหน่งพร้อมกันดูได้ที่ CodePen
  • ตัวอย่างแอนิเมชันสีข้อความอยู่ที่ CodePen
  • แม้ keyframes อาจดูตรงไปตรงมามากกว่า แต่ในฉากเดโมที่มีการคำนวณทางคณิตศาสตร์และตัวแปรนำเข้าจำนวนมาก แนวทางที่ใช้เวลาเป็นตัวแปร จะสร้างผลลัพธ์ที่หลากหลายได้ง่ายกว่า
  • ในอัปเดตวันที่ 17 พฤษภาคม 2024 มีการเพิ่มเดโมแอนิเมชันสีข้อความ

1 ความคิดเห็น

 
GN⁺ 2024-05-06
ความคิดเห็นบน Hacker News
  • ถ้าต้องการควบคุมความคืบหน้าด้วย JS ก็แนะนำให้ใช้ค่า animation-delay แบบติดลบ ด้วย
    เช่น animation-delay: -1500ms จะเริ่มแอนิเมชันทันที แต่กระโดดข้ามไปที่ตำแหน่ง 1.5 วินาที
    ถ้าควบคุมค่านี้ด้วย JS ก็จะสามารถสครับ CSS animation จาก JS ได้ในทางปฏิบัติ ทำให้ซิงก์แอนิเมชันทั้งหมดเข้ากับ tick loop แบบ compute-update-render ของเกมเอนจินได้
    https://developer.mozilla.org/en-US/docs/Web/CSS/animation-d...

    • แบบนี้ก็น่าจะทำ แอนิเมชันตาม scroll offset สไตล์ iOS ได้ด้วยไม่ใช่หรือ?
  • พอซับซ้อนกว่า easing function ง่าย ๆ หรือ keyframe พื้นฐานสักหนึ่งสอง channel นิดเดียว ก็จะชนข้อจำกัดของแนวทางนี้อย่างรวดเร็ว
    ช่วงหลายปีที่ผ่านมาใช้ Theatre.js และค่อนข้างพอใจ
    เป็นไลบรารีที่แบ่งเป็น studio UI สำหรับแก้ไข keyframe และเส้นโค้ง Bezier บน timeline กับ runtime ที่ interpolate keyframe เหล่านั้นตาม timeline
    ถ้าต้องทำให้หลายแอนิเมชันเคลื่อนไหวสอดประสานกัน ก็น่าลองใช้
    https://www.theatrejs.com/

  • น่าประทับใจมาก แต่ก็รู้สึกเหมือนว่าเพิ่งจะเข้าใกล้สิ่งที่ Flash ทำได้เมื่อหลายสิบปีก่อน
    ไม่ได้อยากให้ Flash กลับมา แต่ก็อยากให้มีเครื่องมือที่เป็นมิตรกับผู้ใช้มากกว่านี้สำหรับสร้าง CSS animation

  • ตอนนี้ CSS รองรับ ฟังก์ชันคณิตศาสตร์ อย่าง mod(), round() และฟังก์ชันตรีโกณมิติได้เพียงพอแล้ว
    CSS เริ่ม timer เองแบบ JavaScript ไม่ได้ แต่ทุกวันนี้สามารถใช้ CSS Houdini API กำหนด custom variable ที่ติดตามเวลาเป็นมิลลิวินาทีได้
    แต่ในเมื่อมี JavaScript อยู่แล้ว ทำไมทั้งหมดนี้ถึงจำเป็น?
    เลเยอร์การวาดไม่ควรโฟกัสที่ primitive สำหรับการวาดหรือ? ทำไมถึงใส่ฟีเจอร์ระดับสูงมากขึ้นลงไปตรงนั้น?

    • อย่าลืมว่า CSS animation มี hardware acceleration
      ถ้ารันฟังก์ชันตรีโกณมิติใน JS เพื่อทำแอนิเมชัน ก็น่าจะกำลังปรับ DOM เพื่ออัปเดต UI ทุกมิลลิวินาทีด้วย ซึ่งมีต้นทุนสูงมาก
    • CSS ไม่ใช่ เลเยอร์การวาด
      การวาดถูกขับเคลื่อนโดย DOM ที่ประกอบขึ้นด้วย HTML หรืออัปเดตด้วย JS
      CSS เป็นแค่สิ่งที่วางทับเพื่อส่งพารามิเตอร์ให้ painting engine ว่าจะทำอะไรกับ DOM พร้อมทั้ง override ค่าเริ่มต้น
      มันเพิ่มบล็อก DOM ได้ เปลี่ยนพฤติกรรมการจัดตำแหน่งด้วยอัลกอริทึมที่ซับซ้อนได้ และแม้แต่ margin เองก็ไม่ได้เรียบง่ายขนาดนั้น
      ไม่ได้มองว่าเป็นทางออกที่ยอดเยี่ยม แต่ดูเหมือนเริ่มจากความพยายามที่จะไม่ต้องอัปเดตโค้ด HTML เพียงเพื่อเปลี่ยนสีระหว่างการ render DOM
      คงเพราะการอัปเดตโค้ด HTML ไม่สนุกเลย แต่ผมมองว่าตั้งแต่แรกก็แก้โจทย์ผิดแล้ว
      แน่นอนว่ายุคนั้นเป็นยุคของ “เว็บมาสเตอร์” ด้วย
    • ข้อดีอย่างหนึ่งที่นึกออกคือ การแยก application logic ออกจาก presentation
      ถ้าใน fetch logic ไม่มี logic ความก้าวหน้าทางเรขาคณิตของ loader DOM element ปนอยู่ เช่น การคำนวณมุมหมุนและการใช้ inline style ก็อ่านง่ายกว่า
      รู้ว่าทุกวันนี้วิธีแยกช็อกโกแลตกับเนยถั่วไว้คนละส่วนไม่ค่อยเป็นที่นิยม แต่ convention แบบนี้ช่วยขีดเส้นแบ่งให้เดาได้ง่ายว่าโค้ดควรอยู่ที่ไหน และควรใส่ไว้ตรงไหน
    • โดยพื้นฐานแล้ว JS เป็น single-threaded แม้จะมี worker ก็ตาม ดังนั้นถ้าอยากรักษา UI ให้มี frame rate ที่เสถียร อะไรก็ตามที่โยนงานไปที่อื่นได้ก็เป็นเรื่องดี
    • รอดูจนกว่า WebSocket จะเข้าไปอยู่ใน CSS กันเถอะ!
      เอาเรื่องล้อเล่นไว้ก่อน ผมมองว่า CSS เป็น declarative และเป็นเรื่องธรรมชาติที่ปัญหาซึ่งแก้ได้ด้วย JS กับ CSS จะมีส่วนทับซ้อนกัน
      ฟีเจอร์ใหม่ ๆ ที่ถูกเพิ่มเข้ามาใน CSS ช่วงหลังนี้น่าตื่นเต้นจริง ๆ และการมี interpreted code น้อยลงก็เป็นเรื่องดี
  • CSS @property แบบ custom ที่ใช้ในบทความนั้นยอดเยี่ยม และ browser support ก็อยู่ที่ 88% [1]
    แต่ต้องระวังความแตกต่างของพฤติกรรมแต่ละเบราว์เซอร์เวลาตั้งค่า fallback initial-value
    Chrome จะใช้ initial-value เมื่อ CSS variable ไม่ได้ถูกกำหนด หรือถูกตั้งเป็นค่าที่ไม่ถูกต้อง
    Firefox จะใช้ initial-value เฉพาะตอนที่ variable ไม่ได้ถูกกำหนดเท่านั้น
    สำหรับโปรเจกต์ส่วนใหญ่คงไม่เป็นปัญหา แต่ในโปรเจกต์ล่าสุดต้องตั้งค่า default ด้วย JavaScript ใน Firefox เพื่อให้เข้ากับความต่างของ implementation ระหว่างเบราว์เซอร์
    [1] https://caniuse.com/?search=%40property

    • ถ้าอยากลองเดโมใน Firefox ให้ไปที่ about:config แล้วเปิด layout.css.properties-and-values.enabled
    • วิธีตั้งค่า property value จาก JavaScript เมื่อต้องการมีดังนี้
      ดึงค่า property ที่กำหนดใน :root{}:
      getComputedStyle(document.documentElement).getPropertyValue("--some-custom-css-var");
      ตั้งค่า property value:
      document.documentElement.style.setProperty("--some-custom-css-var", "some value");
      ในเบราว์เซอร์ที่ยังไม่ได้เปิดการรองรับ CSS @property สามารถใช้เหมือน polyfill ได้
  • บน Android Chrome รุ่นล่าสุด น่าสนใจที่เดโมก่อนหน้า mod() ทำงานได้โดยไม่ต้องใช้ Experimental feature flags แต่ mod() กับ sin() ไม่เกิดอะไรขึ้น

  • เหตุผลที่อยากได้สิ่งนี้มานาน คืออยากสร้างอะไรอย่าง box-breathing
    อยากปรับขั้นตอนหายใจเข้า-หยุด-หายใจออก-หยุด ให้เข้ากับร่างกายและเอฟเฟกต์ที่ต้องการได้ทั้งหมด
    ถ้าไม่มีวิธีแบบในหน้านี้ ก็หาวิธีทำโดยไม่ใช้ JS หรือไม่ให้ซับซ้อนแบบเหลวไหลไม่ได้เลย
    ให้ตายสิ นี่มัน stylesheet นะ! เดโมในหน้านี้สร้างสรรค์จริง ๆ

    • นั่นก็แค่ animation curve ของ CSS ไม่ใช่หรือ?
  • บทความเขียนดีมากจริง ๆ
    Chrome ยังไม่ได้ปล่อยการรองรับ CSS mod() เป็น release ทางการ ดังนั้นถ้าไม่ได้ใช้ Chrome รุ่น preview ตัวอย่างส่วนใหญ่ในหน้าจะไม่เกิดแอนิเมชัน
    ถ้าไม่ต้องควบคุม tick และแค่อยากได้ CSS animation ที่ลื่นในทุกเบราว์เซอร์ เทคนิค FLIP ก็มีประโยชน์
    https://medium.com/outsystems-experts/flip-your-60-fps-anima...

  • แอนิเมชันท้ายหน้าประทับใจมาก
    ดูเหมือนเป็น วิดีโอ หนึ่งชิ้นมากกว่าเป็นองค์ประกอบแยก ๆ

  • https://RTCode.io เป็น เว็บเพลย์กราวด์แบบเรียลไทม์ ที่ทำงานได้โดยไม่ต้องรีเฟรชและไม่ต้องรีเซ็ตสถานะ จึงน่าจะช่วยได้มากสำหรับคนที่ทำงานกับ CSS animation หรือ transition
    เดโมอยู่ที่ https://RTCode.io
    ถ้าจะนำเข้า Pen ให้เปลี่ยน hostname ใน URL เป็น xcodepen.io

    • มีใครเคยใช้ RTCode ไหม? ดูน่าสนใจ แต่พอดูหน้าเว็บแล้วแอบสับสนเล็กน้อยว่าเป็นทางเลือกแทน CodePen ที่มีการรองรับแบบเรียลไทม์ หรือว่าสามารถใช้ใน production ได้ด้วย