การสร้างภาพเคลื่อนไหวแบบขยายและยุบ

Stephen McGruer
Stephen McGruer

TL;DR

ใช้การเปลี่ยนรูปแบบขนาดเมื่อสร้างภาพเคลื่อนไหวของคลิป คุณสามารถป้องกันไม่ให้เด็กๆ ยืดออกและบิดเบี้ยวขณะแสดงภาพเคลื่อนไหวด้วยการปรับสเกล

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

ลองดูตัวอย่างเมนูแบบขยายต่อไปนี้

ตัวเลือกบางอย่างในการสร้างนี้จะมีประสิทธิภาพมากกว่าตัวเลือกอื่นๆ

ไม่เหมาะสม: การทำภาพเคลื่อนไหวความกว้างและความสูงในองค์ประกอบคอนเทนเนอร์

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

.menu {
  overflow: hidden;
  width: 350px;
  height: 600px;
  transition: width 600ms ease-out, height 600ms ease-out;
}

.menu--collapsed {
  width: 200px;
  height: 60px;
}

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

ใช้แอตทริบิวต์ CSS clip หรือ clip-path

อีกทางเลือกหนึ่งในการทำให้ width และ height เคลื่อนไหวคือการใช้พร็อพเพอร์ตี้ clip (เลิกใช้งานแล้ว) เพื่อทำให้เอฟเฟกต์การขยายและการยุบเคลื่อนไหว หรือจะใช้ clip-path แทนก็ได้หากต้องการ อย่างไรก็ตาม การใช้ clip-path ได้รับการรองรับน้อยกว่าclip แต่ clip เลิกใช้งานแล้ว ทางขวา แต่ไม่ต้องกังวลไป นี่ไม่ใช่วิธีแก้ปัญหาที่คุณต้องการ

.menu {
  position: absolute;
  clip: rect(0px 112px 175px 0px);
  transition: clip 600ms ease-out;
}

.menu--collapsed {
  clip: rect(0px 70px 34px 0px);
}

แม้ว่าจะดีกว่าการทำให้ width และ height ขององค์ประกอบเมนูเคลื่อนไหวได้ แต่ข้อเสียของแนวทางนี้คือยังคงทริกเกอร์การวาดภาพ นอกจากนี้ พร็อพเพอร์ตี้ clip (หากเลือกใช้) ยังกำหนดให้องค์ประกอบที่ดำเนินการอยู่อยู่ในตำแหน่งแบบสัมบูรณ์หรือแบบคงที่ ซึ่งอาจต้องมีการดัดแปลงเพิ่มเติมเล็กน้อย

ดี: ทำให้สเกลเคลื่อนไหว

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

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

ขั้นตอนที่ 1: คํานวณสถานะเริ่มต้นและสถานะสิ้นสุด

เมื่อใช้แนวทางที่ใช้ภาพเคลื่อนไหวแบบปรับขนาด ขั้นตอนแรกคือการอ่านองค์ประกอบที่บอกขนาดที่เมนูต้องมีทั้งเมื่อยุบอยู่และเมื่อขยายออก ในบางสถานการณ์ คุณอาจไม่สามารถรับข้อมูลทั้ง 2 รายการนี้พร้อมกันได้ และอาจต้องสลับคลาสบางอย่างเพื่ออ่านสถานะต่างๆ ของคอมโพเนนต์ อย่างไรก็ตาม หากจำเป็นต้องทำเช่นนั้น โปรดระมัดระวัง: getBoundingClientRect() (หรือ offsetWidth และ offsetHeight) จะบังคับให้เบราว์เซอร์เรียกใช้สไตล์และเลย์เอาต์หากมีการเปลี่ยนแปลงสไตล์ตั้งแต่เรียกใช้ครั้งล่าสุด

function calculateCollapsedScale () {
    // The menu title can act as the marker for the collapsed state.
    const collapsed = menuTitle.getBoundingClientRect();

    // Whereas the menu as a whole (title plus items) can act as
    // a proxy for the expanded state.
    const expanded = menu.getBoundingClientRect();
    return {
    x: collapsed.width / expanded.width,
    y: collapsed.height / expanded.height
    };
}

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

แต่เดี๋ยวก่อน การดำเนินการนี้จะปรับขนาดเนื้อหาของเมนูด้วยใช่ไหม ใช่ ดังที่คุณเห็นด้านล่าง

คุณจะทำอย่างไรได้บ้าง คุณสามารถใช้การแปลงแบบ counter- กับเนื้อหาได้ ตัวอย่างเช่น หากคอนเทนเนอร์ปรับขนาดลงเป็น 1/5 ของขนาดปกติ คุณก็สามารถปรับขนาดเนื้อหาเพิ่ม 5 เท่าเพื่อป้องกันไม่ให้เนื้อหาถูกบีบ โปรดทราบว่า

  1. ตัวนับการเปลี่ยนรูปแบบยังเป็นการดําเนินการปรับขนาด ซึ่งดีเนื่องจากสามารถเร่งความเร็วได้เช่นเดียวกับภาพเคลื่อนไหวในคอนเทนเนอร์ คุณอาจต้องตรวจสอบว่าองค์ประกอบที่มีภาพเคลื่อนไหวมีเลเยอร์คอมโพสิตของตัวเอง (เพื่อให้ GPU ช่วยทำงาน) ซึ่งคุณทำได้โดยการเพิ่ม will-change: transform ลงในองค์ประกอบ หรือ backface-visiblity: hidden หากต้องการรองรับเบราว์เซอร์รุ่นเก่า

  2. ต้องคำนวณการเปลี่ยนรูปแบบย้อนกลับต่อเฟรม ขั้นตอนนี้อาจซับซ้อนขึ้นเล็กน้อย เนื่องจากสมมติว่าภาพเคลื่อนไหวอยู่ใน CSS และใช้ฟังก์ชันการเปลี่ยนค่าอย่างช้าๆ คุณจะต้องหักลบการเปลี่ยนค่าอย่างช้าๆ นั้นเมื่อสร้างภาพเคลื่อนไหวการเปลี่ยนรูปแบบย้อนกลับ อย่างไรก็ตาม การคำนวณเส้นโค้งผกผันสำหรับ cubic-bezier(0, 0, 0.3, 1) นั้นไม่ชัดเจนนัก

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

ขั้นตอนที่ 2: สร้างภาพเคลื่อนไหว CSS ได้ทันที

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

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

function createKeyframeAnimation () {
    // Figure out the size of the element when collapsed.
    let {x, y} = calculateCollapsedScale();
    let animation = '';
    let inverseAnimation = '';

    for (let step = 0; step <= 100; step++) {
    // Remap the step value to an eased one.
    let easedStep = ease(step / 100);

    // Calculate the scale of the element.
    const xScale = x + (1 - x) * easedStep;
    const yScale = y + (1 - y) * easedStep;

    animation += `${step}% {
        transform: scale(${xScale}, ${yScale});
    }`;

    // And now the inverse for the contents.
    const invXScale = 1 / xScale;
    const invYScale = 1 / yScale;
    inverseAnimation += `${step}% {
        transform: scale(${invXScale}, ${invYScale});
    }`;

    }

    return `
    @keyframes menuAnimation {
    ${animation}
    }

    @keyframes menuContentsAnimation {
    ${inverseAnimation}
    }`;
}

ผู้ที่มีความอยากรู้อยากเห็นไม่รู้จบอาจสงสัยเกี่ยวกับฟังก์ชัน ease() ภายในวง for คุณสามารถใช้รูปแบบคำสั่งนี้เพื่อแมปค่าจาก 0 ถึง 1 กับค่าที่เทียบเท่าแบบเบาลง

function ease (v, pow=4) {
  return 1 - Math.pow(1 - v, pow);
}

คุณใช้การค้นหาของ Google เพื่อพล็อตหน้าตาได้เช่นกัน มีประโยชน์ หากต้องการใช้สมการการโจมตีแบบอื่นๆ โปรดดู Tween.js โดย Soledad Penadés ซึ่งมีสมการโจมตีมากมาย

ขั้นตอนที่ 3: เปิดใช้ภาพเคลื่อนไหว CSS

เมื่อสร้างภาพเคลื่อนไหวเหล่านี้และนำไปใช้กับหน้าเว็บใน JavaScript แล้ว ขั้นตอนสุดท้ายคือการสลับคลาสที่เปิดใช้ภาพเคลื่อนไหว

.menu--expanded {
  animation-name: menuAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

.menu__contents--expanded {
  animation-name: menuContentsAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

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

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

const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;

เวอร์ชันขั้นสูงขึ้น: แสดงด้วยวงกลม

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

หลักการส่วนใหญ่จะเหมือนกันกับเวอร์ชันก่อนหน้า ตรงที่คุณปรับขนาดองค์ประกอบ และปรับขนาดองค์ประกอบย่อยที่อยู่ติดกัน ในกรณีนี้ องค์ประกอบที่กำลังปรับขนาดขึ้นมี border-radius เท่ากับ 50% ทำให้เป็นวงกลมและล้อมรอบด้วยองค์ประกอบอื่นที่มี overflow: hidden ซึ่งหมายความว่าคุณไม่เห็นวงกลมขยายออกนอกขอบเขตขององค์ประกอบ

คำเตือนเกี่ยวกับตัวแปรนี้โดยเฉพาะ: Chrome มีข้อความเบลอบนหน้าจอ DPI ระดับต่ำระหว่างการทำภาพเคลื่อนไหวเนื่องจากข้อผิดพลาดในการปัดเศษเนื่องจากขนาดและมาตราส่วนตัดเปลี่ยนของข้อความ หากสนใจรายละเอียดเกี่ยวกับเรื่องนี้ ระบบได้มีการรายงานข้อบกพร่องไว้ให้คุณติดดาวและติดตามได้

โค้ดสำหรับเอฟเฟกต์การขยายแบบวงกลมอยู่ในที่เก็บ GitHub

บทสรุป

ตอนนี้คุณก็มีวิธีทำคลิปภาพเคลื่อนไหวที่มีประสิทธิภาพโดยใช้การแปลงสัดส่วนแล้ว คงจะดีไม่น้อยหากเราได้เห็นว่าคลิปภาพเคลื่อนไหวเป็นแบบเร่งเร็วขึ้น (ซึ่งมีข้อบกพร่องของ Chromium ที่ผลงานของ Jake Archibald) แต่จนกว่าเราจะไปถึงจุดนั้น คุณควรระมัดระวังเมื่อทำให้ clip หรือ clip-path เคลื่อนไหว และหลีกเลี่ยงการเคลื่อนไหว width หรือ height อย่างแน่นอน

นอกจากนี้ การใช้ภาพเคลื่อนไหวบนเว็บสำหรับเอฟเฟกต์ดังกล่าวยังมีประโยชน์เนื่องจากมี JavaScript API แต่สามารถเรียกใช้บนเธรดคอมโพสิตได้หากคุณทำให้ transform และ opacity เคลื่อนไหวเท่านั้น ขออภัย การรองรับภาพเคลื่อนไหวบนเว็บยังไม่ดีนัก แต่คุณใช้การปรับปรุงแบบเป็นขั้นเป็นตอนเพื่อใช้ภาพเคลื่อนไหวได้หากมี

if ('animate' in HTMLElement.prototype) {
    // Animate with Web Animations.
} else {
    // Fall back to generated CSS Animations or JS.
}

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

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