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

Stephen McGruer
Stephen McGruer

TL;DR

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

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

ตัวอย่างเช่น เมนูแบบขยาย

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

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

คุณอาจจินตนาการได้ว่าการใช้ 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 instead แทนก็ได้หากต้องการ อย่างไรก็ตาม การใช้ 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) ขนาดตามธรรมชาตินี้แสดงสถานะแบบขยาย ซึ่งหมายความว่าคุณจะต้องสร้างภาพเคลื่อนไหวจากเวอร์ชันที่ปรับขนาดให้เล็กลง (ซึ่งคำนวณไว้ด้านบน) กลับไปยังขนาดตามธรรมชาติ

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

คุณจะทำอย่างไรได้บ้าง คุณสามารถใช้การเปลี่ยนรูปแบบแบบย้อนกลับกับเนื้อหาได้ เช่น หากปรับขนาดคอนเทนเนอร์ให้เล็กลงเป็น 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

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