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

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

TL;DR

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

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

ปัญหา

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

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

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

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

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

ลงไปในหลุมกระต่าย

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

การครอสเฟดคือชุดของการจางเข้าและจางออกของความทึบที่ซ้อนทับกัน เช่น หากเรามีขั้นตอนการเบลอ 4 ขั้นตอน เราจะค่อยๆ เบลอขั้นตอนแรกออกในขณะที่ค่อยๆ เบลอขั้นตอนที่สองเข้าพร้อมกัน เมื่อเลเยอร์ที่ 2 มีความทึบแสง 100% และเลเยอร์แรกมีความทึบแสง 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 ยังคงทำงานอย่างหนักและ ขยายแต่ละเฟรมเป็น ~90 มิลลิวินาที แต่ทำไม เราไม่ได้เปลี่ยนค่าเบลอ อีกต่อไป แต่เปลี่ยนเฉพาะความทึบแสงเท่านั้น จึงเกิดอะไรขึ้น ปัญหานี้เกิดจากลักษณะของเอฟเฟกต์เบลออีกครั้ง ดังที่อธิบายไว้ก่อนหน้านี้ หากมีการโปรโมตและเบลอองค์ประกอบพร้อมกัน GPU จะเป็นผู้ใช้เอฟเฟกต์ ดังนั้นแม้ว่าเราจะ ไม่ได้เคลื่อนไหวค่าเบลออีกต่อไป แต่พื้นผิวเองก็ยังไม่เบลอและ ต้องเบลอใหม่ทุกเฟรมโดย GPU สาเหตุที่อัตราเฟรมแย่ลงกว่าเดิม เป็นเพราะเมื่อเทียบกับการติดตั้งใช้งานแบบง่ายๆ แล้ว GPU ต้องทำงานมากกว่าเดิม เนื่องจากส่วนใหญ่แล้วจะมองเห็นพื้นผิว 2 รายการ ที่ต้องเบลอแยกกัน

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

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

ตอนนี้เรามีพื้นที่ว่างมากมายใน GPU และได้ภาพที่ลื่นไหลที่ 60fps เราทำสำเร็จแล้ว!

การนำไปใช้งานจริง

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

แม้ว่าคนส่วนใหญ่จะคิดว่า Shadow DOM เป็นวิธีแนบองค์ประกอบ "ภายใน" เข้ากับ Custom Elements แต่ก็ยังเป็น การแยกและการเพิ่มประสิทธิภาพขั้นพื้นฐานด้วย 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 หากมีข้อสงสัย