เทคนิคแอนิเมชัน CSS แบบอิงเวลา
(yuanchuan.dev)- เมื่อ 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)))
- 8fps:
เปลี่ยนค่าเวลาที่เพิ่มขึ้นให้เป็นค่าแอนิเมชัน
- ค่า
--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 ความคิดเห็น
ความคิดเห็นบน 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...
พอซับซ้อนกว่า 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 สำหรับการวาดหรือ? ทำไมถึงใส่ฟีเจอร์ระดับสูงมากขึ้นลงไปตรงนั้น?
ถ้ารันฟังก์ชันตรีโกณมิติใน JS เพื่อทำแอนิเมชัน ก็น่าจะกำลังปรับ DOM เพื่ออัปเดต UI ทุกมิลลิวินาทีด้วย ซึ่งมีต้นทุนสูงมาก
การวาดถูกขับเคลื่อนโดย DOM ที่ประกอบขึ้นด้วย HTML หรืออัปเดตด้วย JS
CSS เป็นแค่สิ่งที่วางทับเพื่อส่งพารามิเตอร์ให้ painting engine ว่าจะทำอะไรกับ DOM พร้อมทั้ง override ค่าเริ่มต้น
มันเพิ่มบล็อก DOM ได้ เปลี่ยนพฤติกรรมการจัดตำแหน่งด้วยอัลกอริทึมที่ซับซ้อนได้ และแม้แต่ margin เองก็ไม่ได้เรียบง่ายขนาดนั้น
ไม่ได้มองว่าเป็นทางออกที่ยอดเยี่ยม แต่ดูเหมือนเริ่มจากความพยายามที่จะไม่ต้องอัปเดตโค้ด HTML เพียงเพื่อเปลี่ยนสีระหว่างการ render DOM
คงเพราะการอัปเดตโค้ด HTML ไม่สนุกเลย แต่ผมมองว่าตั้งแต่แรกก็แก้โจทย์ผิดแล้ว
แน่นอนว่ายุคนั้นเป็นยุคของ “เว็บมาสเตอร์” ด้วย
ถ้าใน fetch logic ไม่มี logic ความก้าวหน้าทางเรขาคณิตของ loader DOM element ปนอยู่ เช่น การคำนวณมุมหมุนและการใช้ inline style ก็อ่านง่ายกว่า
รู้ว่าทุกวันนี้วิธีแยกช็อกโกแลตกับเนยถั่วไว้คนละส่วนไม่ค่อยเป็นที่นิยม แต่ convention แบบนี้ช่วยขีดเส้นแบ่งให้เดาได้ง่ายว่าโค้ดควรอยู่ที่ไหน และควรใส่ไว้ตรงไหน
เอาเรื่องล้อเล่นไว้ก่อน ผมมองว่า CSS เป็น declarative และเป็นเรื่องธรรมชาติที่ปัญหาซึ่งแก้ได้ด้วย JS กับ CSS จะมีส่วนทับซ้อนกัน
ฟีเจอร์ใหม่ ๆ ที่ถูกเพิ่มเข้ามาใน CSS ช่วงหลังนี้น่าตื่นเต้นจริง ๆ และการมี interpreted code น้อยลงก็เป็นเรื่องดี
CSS
@propertyแบบ custom ที่ใช้ในบทความนั้นยอดเยี่ยม และ browser support ก็อยู่ที่ 88% [1]แต่ต้องระวังความแตกต่างของพฤติกรรมแต่ละเบราว์เซอร์เวลาตั้งค่า fallback
initial-valueChrome จะใช้
initial-valueเมื่อ CSS variable ไม่ได้ถูกกำหนด หรือถูกตั้งเป็นค่าที่ไม่ถูกต้องFirefox จะใช้
initial-valueเฉพาะตอนที่ variable ไม่ได้ถูกกำหนดเท่านั้นสำหรับโปรเจกต์ส่วนใหญ่คงไม่เป็นปัญหา แต่ในโปรเจกต์ล่าสุดต้องตั้งค่า default ด้วย JavaScript ใน Firefox เพื่อให้เข้ากับความต่างของ implementation ระหว่างเบราว์เซอร์
[1] https://caniuse.com/?search=%40property
about:configแล้วเปิดlayout.css.properties-and-values.enabledดึงค่า 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 นะ! เดโมในหน้านี้สร้างสรรค์จริง ๆ
บทความเขียนดีมากจริง ๆ
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