การทำภาพเคลื่อนไหวเบลอ

การเบลอเป็นวิธีที่ยอดเยี่ยมในการเปลี่ยนจุดสนใจของผู้ใช้ การทําให้องค์ประกอบภาพบางส่วนเบลอขณะที่องค์ประกอบอื่นๆ ยังคงอยู่ในโฟกัสจะดึงดูดความสนใจของผู้ใช้ได้อย่างเป็นธรรมชาติ ผู้ใช้จะมองข้ามเนื้อหาที่เบลอและมุ่งเน้นที่เนื้อหาที่อ่านได้ ตัวอย่างหนึ่งคือรายการไอคอนที่แสดงรายละเอียดเกี่ยวกับแต่ละรายการเมื่อวางเมาส์เหนือ ในระหว่างนี้ ตัวเลือกที่เหลืออาจเบลอเพื่อเปลี่ยนเส้นทางผู้ใช้ไปยังข้อมูลที่แสดงใหม่

TL;DR

การทำภาพเบลอเป็นภาพเคลื่อนไหวไม่ใช่ตัวเลือกที่ดีนักเนื่องจากทำงานได้ช้ามาก แต่ระบบจะประมวลผลชุดภาพที่เบลอมากขึ้นเรื่อยๆ ล่วงหน้า แล้วค่อยค่อยเบลอภาพเหล่านั้นให้ซ้อนทับกัน เพื่อนร่วมงานของฉัน Yi Gu ได้เขียนคลังขึ้นมาเพื่อดูแลทุกอย่างให้คุณ โปรดดูเดโม

อย่างไรก็ตาม เทคนิคนี้อาจทำให้ผู้ใช้ตกใจเมื่อใช้โดยไม่มีระยะเวลาเปลี่ยนผ่าน การทำภาพเบลอเป็นภาพเคลื่อนไหว (การเปลี่ยนจากภาพคมชัดเป็นภาพเบลอ) ดูเหมือนจะเป็นทางเลือกที่สมเหตุสมผล แต่หากคุณเคยลองทำสิ่งนี้บนเว็บ คุณอาจพบว่าภาพเคลื่อนไหวนั้นไม่ลื่นไหลอย่างที่คิด ดังที่ตัวอย่างนี้แสดงให้คุณเห็นหากคุณไม่มีเครื่องที่มีประสิทธิภาพ เราช่วยปรับปรุงอะไรได้อีกไหม

ปัญหา

CPU จะเปลี่ยนมาร์กอัปให้เป็นพื้นผิว ระบบจะอัปโหลดพื้นผิวไปยัง GPU GPU จะวาดพื้นผิวเหล่านี้ไปยังเฟรมบัฟเฟอร์โดยใช้โปรแกรมเปลี่ยนรูปแบบ การเบลอจะเกิดขึ้นในโปรแกรมเปลี่ยนสี

ในตอนนี้ เราไม่สามารถทำให้ภาพเบลอเคลื่อนไหวได้อย่างมีประสิทธิภาพ อย่างไรก็ตาม เราหาวิธีแก้ปัญหาที่ดูดีพอได้ แต่ในทางเทคนิคแล้วไม่ใช่ภาพเบลอแบบเคลื่อนไหว ก่อนเริ่มต้น มาดูกันก่อนว่าทำไมการเบลอแบบเคลื่อนไหวจึงทำงานช้า การเบลอองค์ประกอบบนเว็บทำได้ 2 วิธี ได้แก่ filter คุณสมบัติ CSS และฟิลเตอร์ SVG โดยปกติแล้วจะใช้ตัวกรอง CSS เนื่องจากได้รับการสนับสนุนมากขึ้นและใช้งานง่าย ขออภัย หากจำเป็นต้องรองรับ Internet Explorer คุณไม่มีทางเลือกอื่นนอกจากใช้ฟิลเตอร์ SVG เนื่องจาก IE 10 และ 11 รองรับฟิลเตอร์ดังกล่าว แต่ไม่รองรับฟิลเตอร์ CSS ข่าวดีคือวิธีแก้ปัญหาเฉพาะหน้าสำหรับการสร้างภาพเบลอแบบเคลื่อนไหวใช้ได้กับทั้ง 2 เทคนิค เรามาลองหาจุดคอขวดโดยดูที่เครื่องมือสำหรับนักพัฒนาเว็บกัน

หากเปิดใช้ "การกะพริบของภาพ" ในเครื่องมือสำหรับนักพัฒนาเว็บ คุณจะไม่เห็นการกะพริบใดๆ เลย ดูเหมือนว่าจะไม่มีภาพวาดใหม่ ซึ่งถูกต้องตามเทคนิคเนื่องจาก "การวาดภาพใหม่" หมายถึงการที่ CPU ต้องวาดภาพพื้นผิวขององค์ประกอบที่โปรโมตใหม่ เมื่อใดก็ตามที่องค์ประกอบได้รับการโปรโมตและเบลอ GPU จะใช้การเบลอโดยใช้โปรแกรมเปลี่ยนสี

ทั้งฟิลเตอร์ SVG และฟิลเตอร์ CSS ใช้ฟิลเตอร์การกรองเพื่อเบลอ ตัวกรองการกรองข้อมูลแบบกรวยค่อนข้างมีราคาแพง เนื่องจากต้องพิจารณาจำนวนพิกเซลอินพุตสำหรับพิกเซลเอาต์พุตแต่ละพิกเซล ยิ่งรูปภาพหรือรัศมีเบลอมีขนาดใหญ่เท่าใด เอฟเฟกต์ก็จะยิ่งใช้ทรัพยากรมากเท่านั้น

และนี่ก็คือปัญหา เรากำลังใช้การดำเนินการ GPU ที่ค่อนข้างแพงในทุกเฟรม ซึ่งทำให้เฟรมของเราเกินงบประมาณ 16 มิลลิวินาที และทำให้เฟรมมีอัตราเฟรมต่ำกว่า 60fps

เจาะลึก

เราจะช่วยอะไรได้บ้างเพื่อให้การดำเนินการนี้ราบรื่น เราใช้เทคนิคมือไวตาไวได้ แทนที่จะแสดงผลค่าเบลอจริง (รัศมีของการเบลอ) แบบเคลื่อนไหว เราคำนวณสำเนาเบลอ 2 รายการล่วงหน้าโดยให้ค่าเบลอเพิ่มขึ้นแบบทวีคูณ จากนั้นค่อยๆ เปลี่ยนจากสำเนาหนึ่งไปยังอีกสำเนาหนึ่งโดยใช้ opacity

การเฟดข้ามคือชุดของเฟดอินและเฟดเอาท์ความทึบซ้อนทับกัน เช่น หากมีระยะเบลอ 4 ระยะ เราจะค่อยๆ เบลอระยะแรกออกขณะที่ค่อยๆ เบลอระยะที่ 2 เข้าพร้อมกัน เมื่อเฟสที่ 2 มีความทึบ 100% และเฟสที่ 1 มีความทึบ 0% เราจะค่อยๆ เลือนเฟสที่ 2 ออกขณะค่อยๆ เลือนเฟสที่ 3 เข้า เมื่อดำเนินการเสร็จแล้ว เราจะค่อยๆ เลือนเฟรมของเวอร์ชันที่ 3 ออก แล้วเลือนเฟรมของเวอร์ชันที่ 4 ซึ่งเป็นเวอร์ชันสุดท้ายเข้ามา ในกรณีนี้ แต่ละระยะจะใช้เวลา ¼ ของระยะเวลาที่ต้องการทั้งหมด ในแง่ภาพ วิดีโอนี้มีลักษณะคล้ายกับภาพเบลอแบบเคลื่อนไหวจริงมาก

จากการทดสอบของเรา การเพิ่มรัศมีเบลอแบบทวีคูณในแต่ละระยะจะให้ผลลัพธ์ภาพที่ดีที่สุด ตัวอย่างเช่น หากมีระยะเบลอ 4 ระยะ เราจะใช้ filter: blur(2^n) กับแต่ละระยะ เช่น ระยะที่ 0: 1 พิกเซล, ระยะที่ 1: 2 พิกเซล, ระยะที่ 2: 4 พิกเซล และระยะที่ 3: 8 พิกเซล หากเราบังคับให้สำเนาที่เบลอแต่ละรายการนี้อยู่ในเลเยอร์ของตัวเอง (เรียกว่า "การโปรโมต") โดยใช้ will-change: transform การเปลี่ยนความทึบขององค์ประกอบเหล่านี้ควรจะรวดเร็วมาก ในทางทฤษฎีแล้ว วิธีนี้จะช่วยให้เราดำเนินการเบลอภาพที่มีค่าใช้จ่ายสูงได้ตั้งแต่ต้น แต่ปรากฏว่าตรรกะนี้มีข้อบกพร่อง หากเรียกใช้การสาธิตนี้ คุณจะเห็นอัตราเฟรมยังคงต่ำกว่า 60 fps และภาพเบลอจะแย่ลงกว่าก่อนหน้านี้

DevTools
  แสดงการติดตามที่ GPU มีเวลาทำงานเป็นเวลานาน

จากการตรวจสอบ DevTools อย่างรวดเร็วพบว่า GPU ยังทำงานอย่างหนักและยืดแต่ละเฟรมเป็น ~90ms แต่ทำไม เราไม่ได้เปลี่ยนค่าเบลออีกต่อไป แต่จะเปลี่ยนเฉพาะระดับความทึบเท่านั้น เกิดอะไรขึ้น ปัญหานี้เกิดจากลักษณะของเอฟเฟกต์เบลออีกเช่นเคย ดังที่ได้อธิบายไปก่อนหน้านี้ หากองค์ประกอบได้รับการโปรโมตและเบลอ GPU จะใช้เอฟเฟกต์ดังกล่าว ดังนั้นแม้ว่าเราจะไม่ได้ทำให้ค่าเบลอเคลื่อนไหวอีกต่อไป แต่พื้นผิวก็ยังคงไม่เบลออยู่และ GPU จะต้องเบลอพื้นผิวใหม่ทุกเฟรม สาเหตุที่อัตราเฟรมแย่ลงกว่าเดิมนั้นมาจากการที่ GPU มีงานมากกว่าเดิมเมื่อเทียบกับการใช้งานแบบง่าย เนื่องจากส่วนใหญ่จะเห็นพื้นผิว 2 รายการที่ต้องเบลอแยกกัน

สิ่งที่เราคิดขึ้นมานั้นไม่สวยนัก แต่ทำให้ภาพเคลื่อนไหวเร็วสุดๆ เราจะกลับไปไม่โปรโมตองค์ประกอบที่จะเบลอ แต่โปรโมต Wrapper หลักแทน หากองค์ประกอบทั้งเบลอและได้รับการโปรโมต GPU จะใช้เอฟเฟกต์ การดำเนินการนี้ทำให้เดโมทำงานช้า หากองค์ประกอบเบลอแต่ไม่ได้โปรโมต ระบบจะแรสเตอร์เบลอเป็นพื้นผิวหลักที่ใกล้ที่สุดแทน ในกรณีของเรา องค์ประกอบดังกล่าวคือองค์ประกอบโฆษณาแบบกรอกฟอร์ม ตอนนี้รูปภาพที่เบลอจะกลายเป็นพื้นผิวขององค์ประกอบหลักและสามารถนำไปใช้กับเฟรมในอนาคตทั้งหมดซ้ำได้ ซึ่งได้ผลก็เพราะเราทราบว่าองค์ประกอบที่เบลอนั้นไม่มีภาพเคลื่อนไหว และการแคชองค์ประกอบดังกล่าวจึงมีประโยชน์ ต่อไปนี้เป็นตัวอย่างที่ใช้เทคนิคนี้ ฉันสงสัยว่า Moto G4 จะคิดอย่างไรกับแนวทางนี้ ระวังสปอยล์: โทรศัพท์คิดว่าคุณภาพดีมาก

DevTools
  แสดงการติดตามที่ GPU มีเวลาว่างมาก

ตอนนี้เรามีพื้นที่เหลือเฟือใน GPU และเล่นได้อย่างราบรื่นที่ 60 fps เราทำได้

การสร้างเวอร์ชันที่ใช้งานจริง

ในการแสดงตัวอย่างนี้ เราได้ทำซ้ำโครงสร้าง DOM หลายครั้งเพื่อให้มีสำเนาของเนื้อหาที่จะเบลอในระดับความเข้มที่แตกต่างกัน คุณอาจสงสัยว่าวิธีนี้จะทำงานอย่างไรในสภาพแวดล้อมที่ใช้งานจริง เนื่องจากอาจส่งผลข้างเคียงที่ไม่คาดคิดกับสไตล์ CSS หรือแม้แต่ JavaScript ของผู้เขียน ถูกต้อง พบกับ Shadow DOM

แม้ว่าผู้ใช้ส่วนใหญ่จะคิดว่า Shadow DOM เป็นวิธีแนบองค์ประกอบ "ภายใน" กับองค์ประกอบที่กําหนดเอง แต่แท้จริงแล้ว Shadow DOM ยังเป็นรูปแบบการแยกและประสิทธิภาพขั้นพื้นฐานด้วย JavaScript และ CSS ไม่สามารถเจาะขอบเขต Shadow DOM ซึ่งช่วยให้เราทำซ้ำเนื้อหาได้โดยไม่รบกวนรูปแบบหรือตรรกะแอปพลิเคชันของโปรแกรมเมอร์ เรามีองค์ประกอบ <div> สำหรับแต่ละสำเนาที่จะแรสเตอร์อยู่แล้ว และตอนนี้ใช้ <div> เหล่านี้เป็นโฮสต์เงา เราจะสร้าง ShadowRoot โดยใช้ attachShadow({mode: 'closed'}) และแนบสำเนาเนื้อหาไปกับ ShadowRoot แทน <div> เราต้องตรวจสอบว่าได้คัดลอกสไตล์ชีตทั้งหมดไปยัง ShadowRoot ด้วยเพื่อให้มั่นใจว่าสำเนาของเราจะมีสไตล์เหมือนกับต้นฉบับ

เบราว์เซอร์บางรุ่นไม่รองรับ Shadow DOM v1 และสำหรับเบราว์เซอร์เหล่านั้น เราจะใช้วิธีสำเนาเนื้อหาและหวังว่าทุกอย่างจะทำงานได้ตามปกติ เราสามารถใช้ Shadow DOM polyfill กับ ShadyCSS ได้ แต่ไม่ได้ติดตั้งใช้งานในไลบรารี

ทีนี้คุณก็ทราบแล้ว หลังจากท่องไปในไปป์ไลน์การแสดงผลของ Chrome เราค้นพบวิธีสร้างภาพเบลอแบบเคลื่อนไหวอย่างมีประสิทธิภาพในเบราว์เซอร์ต่างๆ

บทสรุป

คุณไม่ควรใช้เอฟเฟกต์ประเภทนี้อย่างพร่ำเพรื่อ เนื่องจากเราคัดลอกองค์ประกอบ DOM และบังคับให้แสดงในเลเยอร์ของตัวเอง เราจึงสามารถเพิ่มขีดจำกัดของอุปกรณ์ระดับล่างได้ การคัดลอกสไตล์ชีตทั้งหมดไปยัง ShadowRoot แต่ละรายการอาจเป็นความเสี่ยงด้านประสิทธิภาพด้วย คุณจึงควรตัดสินใจว่าต้องการปรับตรรกะและสไตล์เพื่อไม่ให้ได้รับผลกระทบจากสำเนาใน LightDOM หรือจะใช้เทคนิค ShadowDOM ของเรา แต่บางครั้งเทคนิคของเราก็อาจคุ้มค่าที่จะลงทุน โปรดดูโค้ดในที่เก็บ GitHub รวมถึงเดโม และหากมีข้อสงสัยใดๆ โปรดติดต่อเราทาง Twitter