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

Nov 25 2022
ฉันชอบติดตามคนที่สร้างแรงบันดาลใจบนอินเทอร์เน็ตและดูสิ่งที่พวกเขาสร้าง บุคคลหนึ่งคือ Cassie Codes เธอสร้างแอนิเมชั่นที่น่าทึ่งสำหรับเว็บ หนึ่งในตัวอย่างที่สร้างแรงบันดาลใจของเธอคือแมงกะพรุนแอนิเมชั่นที่น่ารักตัวนี้

ฉันชอบติดตามผู้คนที่สร้างแรงบันดาลใจบนอินเทอร์เน็ตและดูสิ่งที่พวกเขาสร้าง — หนึ่งในนั้นคือCassie Codesเธอสร้างแอนิเมชั่นที่น่าทึ่งสำหรับเว็บ หนึ่งในตัวอย่างที่สร้างแรงบันดาลใจของเธอคือแมงกะพรุนแอนิเมชั่นน่ารักตัวนี้

หลังจากเห็นสิ่งนี้และหมกมุ่นอยู่กับมันสักพัก ฉันก็คิดกับตัวเองเสมอว่าสัตว์น้อยน่ารักตัวนี้จะต้องมีชีวิตขึ้นมาใน Compose ด้วย ดังนั้นโพสต์บล็อกนี้จึงอธิบายวิธีที่ฉันดำเนินการใน Jetpack Compose รหัสสุดท้ายสามารถพบได้ที่นี่ เทคนิคในที่นี่ไม่ได้เกี่ยวข้องกับแมงกะพรุนเท่านั้นแน่นอน… ปลาอื่น ๆ ก็ทำเช่นกัน! ล้อเล่น — โพสต์บล็อกนี้จะครอบคลุมถึง:

  • ImageVectors ที่กำหนดเอง
  • การสร้างภาพเคลื่อนไหวเส้นทางหรือกลุ่ม ImageVector
  • การใช้เอฟเฟ็กต์เสียงที่บิดเบี้ยวบน Composable ด้วย AGSL RenderEffect

กำลังวิเคราะห์ SVG

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

ลองมาดูภาพรวมของ SVG นี้กัน:

<!-- 
    Jellyfish SVG, path data removed for brevity 
--> 
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 530.46 563.1">
  <defs>
  <filter id="turbulence" filterUnits="objectBoundingBox" x="0" y="0" width="100%" height="100%">
    <feTurbulence data-filterId="3" baseFrequency="0.02 0.03" result="turbulence" id="feturbulence" type="fractalNoise" numOctaves="1" seed="1"></feTurbulence>
    <feDisplacementMap id="displacement" xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" in2="turbulence" scale="13" />
  </filter>    
  </defs>
  <g class="jellyfish" filter="url(#turbulence)">
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="tentacle" />
    <path class="tentacle" />
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="face" />
    <path class="outerJelly"/>
    <path id="freckle" />
    <path id="freckle"/>
    <path id="freckle-4"/>
  </g>
  <g id="bubbles" fill="#fff">
    <path class="bubble"/>
    <path class="bubble"/>
    <path class="bubble" />
    <path class="bubble"/>
    <path class="bubble"/>
    <path class="bubble"/>
    <path class="bubble" />
  </g>
  <g class="jellyfish face">
    <path class="eye lefteye"  fill="#b4bebf" d=""/>
    <path class="eye righteye" fill="#b4bebf" d=""/>
    <path class="mouth" fill="#d3d3d3" opacity=".72"/>
  </g>
</svg>

  1. เส้นทางและกลุ่มของเส้นทางที่ประกอบเป็น SVG:
  2. หนวด
  3. ใบหน้า — หยดและเจลลี่ด้านนอก
  4. ตา — สิ่งมีชีวิตเปิดและปิด
  5. ฟองอากาศ—เคลื่อนไหวแบบสุ่มรอบๆ แมงกะพรุน—ขนาดและอัลฟ่าเคลื่อนไหว
  6. ตอนนี้เราเข้าใจแล้วว่า SVG นี้ประกอบขึ้นจากอะไร เรามาดำเนินการเรนเดอร์เวอร์ชันสแตติกในการเขียน

    การสร้าง ImageVector แบบกำหนดเอง

    เขียนมีแนวคิดของImageVectorซึ่งคุณสามารถสร้างเวกเตอร์โดยทางโปรแกรม — คล้ายกับ SVG สำหรับเวกเตอร์/SVG ที่คุณต้องการแสดงผลโดยไม่เปลี่ยนแปลง คุณยังสามารถโหลด VectorDrawable โดยใช้ painterResource(R.drawable.vector_image) สิ่งนี้จะดูแลการแปลงเป็น ImageVector ที่ Compose จะแสดงผล

    ตอนนี้คุณอาจถามตัวเองว่าทำไมไม่เพียงแค่นำเข้าแมงกะพรุนเป็น SVG ลงในไฟล์ xml แล้วโหลดโดยใช้painterResource(R.drawable.jelly_fish)?

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

    ในการเรนเดอร์แมงกะพรุนนี้ในการเขียน เราสามารถคัดลอกข้อมูลพาธ (หรือdแท็ก “ ” บนพาธ) ที่ประกอบกันเป็นปลาได้ เช่น หนวดแรกมีข้อมูลพาธดังต่อไปนี้:

    M226.31 258.64c.77 8.68 2.71 16.48 1.55 25.15-.78 8.24-5 15.18-7.37 23-3.1 10.84-4.65 22.55 1.17 32.52 4.65 7.37 7.75 11.71 5.81 21.25-2.33 8.67-7.37 16.91-2.71 26 4.26 8.68 7.75 4.34 8.14-3 .39-12.14 0-24.28.77-36 .78-16.91-12-27.75-2.71-44.23 7-12.15 11.24-33 7.76-46.83z
    

    • M, m : ย้ายไปที่
    • L, l, H, h, V, v : บรรทัดที่
    • C, c, S, s : ลูกบาศก์เบซิเยร์โค้งถึง
    • Q, q, T, t:เส้นโค้งเบซิเยร์กำลังสองถึง
    • A, a:เส้นโค้งวงรีโค้งถึง
    • Z, z — ปิดเส้นทาง

    ตอนนี้คุณอาจกำลังคิดว่า - ฉันต้องวาดภาพในหัวของฉันและรู้ตำแหน่งและคำสั่งทั้งหมดด้วยมือหรือไม่? ไม่เลย. คุณสามารถสร้างเวกเตอร์ได้ในโปรแกรมออกแบบส่วนใหญ่ เช่น Figma หรือ Inkscape และส่งออกผลงานภาพวาดของคุณไปยัง SVG เพื่อรับข้อมูลนี้ด้วยตัวคุณเอง ต๊าย!

    ในการสร้างเวกเตอร์ในการเขียน: เราเรียกrememberVectorPainter, ซึ่งสร้างImageVector, และเราสร้างเวกเตอร์ที่Groupเรียก , จากนั้นจึง เรียกjellyfishอีกอันหนึ่งและเราวางอันแรกไว้ข้างในสำหรับหนวดแรก เรายังตั้งค่าเป็นพื้นหลังสำหรับแมงกะพรุนทั้งหมดGrouptentaclesPathRadialGradient

    และผลลัพธ์ต่อไปนี้คือหนวดขนาดเล็กที่วาดบนหน้าจอพร้อมพื้นหลังไล่ระดับสีแบบรัศมี!

    การแสดงหนวดครั้งแรก

    เราทำซ้ำขั้นตอนนี้สำหรับองค์ประกอบทั้งหมดของ SVG โดยนำบิตของเส้นทางจากไฟล์ SVG และใช้สีและอัลฟากับเส้นทางที่จะวาด นอกจากนี้เรายังจัดกลุ่มเส้นทางตามเหตุผลเป็นหนวด ใบหน้า ฟองอากาศ ฯลฯ:

    ตอนนี้เรามีแมงกะพรุนทั้งหมดของเราที่แสดงด้านบนImageVector:

    การแสดงผลแมงกะพรุนคงที่ทั้งหมดในการเขียน

    การสร้างภาพเคลื่อนไหว ImageVector Paths และ Groups

    เราต้องการให้ส่วนต่างๆ ของเวกเตอร์นี้เคลื่อนไหว:

    • แมงกะพรุนควรเคลื่อนที่ขึ้นและลงอย่างช้าๆ
    • ตาควรกะพริบเมื่อคลิกที่แมงกะพรุน
    • ตัวแมงกะพรุนควรมีเอฟเฟกต์สั่นคลอน/เสียง

    มาดูกันว่าเราจะทำให้ส่วนต่างๆ ของไฟล์ImageVector.

    ย้ายแมงกะพรุนขึ้นและลง

    เมื่อดูที่ codepen เราจะเห็นว่าแมงกะพรุนกำลังเคลื่อนไหวโดยมีการแปลขึ้นและลง (y แปล) เมื่อต้องการทำสิ่งนี้ในการเขียน เราสร้างการเปลี่ยนแปลงที่ไม่สิ้นสุดและสิ่งtranslationYที่จะเคลื่อนไหวมากกว่า 3,000 มิลลิวินาที จากนั้นเราตั้งค่ากลุ่มที่มีแมงกะพรุน และใบหน้าให้มี a translationYสิ่งนี้จะสร้างภาพเคลื่อนไหวขึ้นและลง

    การแปลขึ้นและลง

    เยี่ยม — ส่วนหนึ่งของImageVectorตอนนี้เคลื่อนไหวขึ้นและลง คุณจะสังเกตเห็นว่าฟองอากาศยังคงอยู่ในตำแหน่งเดิม

    ตากระพริบ ️

    เมื่อมองไปที่ codepen เราจะเห็นว่ามีscaleYและopacityภาพเคลื่อนไหวที่ดวงตาแต่ละข้าง ลองสร้างตัวแปรสองตัวนี้และใช้สเกลกับ the Groupและ alpha บนPath. นอกจากนี้ เราจะใช้สิ่งเหล่านี้เมื่อคลิกแมงกะพรุนเท่านั้น เพื่อทำให้เป็นแอนิเมชั่นแบบอินเทอร์แอกทีฟมากขึ้น

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

    ตอนนี้เรามีแอนิเมชั่นกะพริบน่ารักเมื่อคลิก — และแมงกะพรุนของเราก็ใกล้จะเสร็จสมบูรณ์แล้ว!

    กะพริบเมื่อคลิก ImageVector

    การใช้เอฟเฟ็กต์การบิดเบือน/สัญญาณรบกวน

    ดังนั้นเราจึงมีสิ่งส่วนใหญ่ที่เราต้องการให้เคลื่อนไหว — การเคลื่อนไหวขึ้นและลง และการกะพริบ มาดูกันว่าร่างกายของแมงกะพรุนมีผลการโคลงเคลงอย่างไร ร่างกายและหนวดจะเคลื่อนไหวพร้อมกับเสียงที่กระทบกับพวกมันเพื่อให้รู้สึกถึงการเคลื่อนไหว

    Codepen: แมงกะพรุนที่ไม่มีเสียงรบกวนเทียบกับที่ใช้เสียงรบกวน

    เมื่อดูที่ SVG และโค้ดแอนิเมชัน เราจะเห็นว่ามันใช้feTurbulenceสร้างสัญญาณรบกวนที่นำไปใช้กับ SVG เป็นไฟล์feDisplacementMap.

    <filter id="turbulence" filterUnits="objectBoundingBox" x="0" y="0" width="100%" height="100%">
        <feTurbulence data-filterId="3" baseFrequency="0.02 0.03" result="turbulence" id="feturbulence" type="fractalNoise" numOctaves="1" seed="1"></feTurbulence>
        <feDisplacementMap id="displacement" xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" in2="turbulence" scale="13" />
      </filter>    
      </defs>
      <g class="jellyfish" filter="url(#turbulence)">
    

    เราสามารถใช้AGSL shaders เพื่อให้ได้สิ่งนี้ เป็นที่น่าสังเกตว่าสิ่งนี้รองรับเฉพาะใน Tiramisu ขึ้นไป (API 33+) อันดับแรก เราต้องสร้าง shader ที่จะทำหน้าที่โยกเยก เราจะไม่ใช้สัญญาณรบกวนในตอนแรก — เพียงแค่ใช้ฟังก์ชันการทำแผนที่แทนเพื่อความเรียบง่าย

    วิธีการทำงานของ shaders คือทำหน้าที่กับแต่ละพิกเซล — เราได้รับพิกัด ( fragCoord) และเราคาดว่าจะสร้างผลลัพธ์สีที่จะแสดงผลที่พิกัดนั้น ด้านล่างคือ shader เริ่มต้นที่เราจะใช้สำหรับการแปลงองค์ประกอบ:

    ในกรณีของเรา อินพุตที่เราจะใช้คือพิกเซลที่แสดงบนหน้าจอในปัจจุบัน เราสามารถเข้าถึงได้ผ่านuniform shader contents;ตัวแปรที่เราจะส่งเป็นอินพุต เราใช้พิกัดอินพุต ( fragCoord) และเราใช้การแปลงบางอย่างกับพิกัดนี้ — ย้ายมันตามเวลาและโดยทั่วไปแล้วคำนวณเลขเพื่อย้ายมันไปรอบๆ

    สิ่งนี้สร้างพิกัดใหม่ ดังนั้นแทนที่จะส่งคืนสีที่แน่นอนที่fragCoordตำแหน่ง เราจะเปลี่ยนตำแหน่งที่เราได้รับพิกเซลอินพุต ตัวอย่างเช่น ถ้าเรามีreturn contents.eval(fragCoord)ก็จะไม่เกิดการเปลี่ยนแปลง มันจะเป็นทางผ่าน ตอนนี้เราได้สีพิกเซลจากจุดอื่นขององค์ประกอบ ซึ่งจะสร้างเอฟเฟกต์การบิดเบือนที่สั่นคลอนในเนื้อหาขององค์ประกอบ

    หากต้องการใช้สิ่งนี้กับองค์ประกอบของเรา เราสามารถใช้ shader นี้เป็น a RenderEffectกับเนื้อหาขององค์ประกอบองค์ประกอบภาพ:

    เราใช้createRuntimeShaderEffect, ส่งผ่านWOBBLE_SHADERเป็นอินพุต สิ่งนี้ใช้เนื้อหาปัจจุบันขององค์ประกอบและจัดเตรียมเป็นอินพุตใน shader โดยมีชื่อพารามิเตอร์ " contents" จากนั้นเราค้นหาเนื้อหาภายในไฟล์WOBBLE_SHADER. ตัวแปรtimeจะเปลี่ยนการโยกเยกเมื่อเวลาผ่านไป (การสร้างภาพเคลื่อนไหว)

    เมื่อรันสิ่งนี้ เราจะเห็นว่าImageตอนนี้ทั้งหมดบิดเบี้ยวและดูสั่นคลอนกว่าเดิมเล็กน้อย — เหมือนแมงกะพรุน

    โยกเยกใส่แมงกะพรุนทั้งตัว

    หากเราไม่ต้องการให้เอฟเฟกต์ใช้กับใบหน้าและฟองอากาศ เราสามารถแยกสิ่งเหล่านั้นออกเป็นส่วนImageVectorsๆ และข้ามการใช้เอฟเฟกต์การเรนเดอร์กับเวกเตอร์เหล่านั้น:

    Wobble ใช้แล้วไม่กระทบผิวหน้า

    การใช้เอฟเฟกต์เสียงรบกวน

    Shader ที่เราระบุไว้ข้างต้นไม่ได้ใช้ฟังก์ชันเสียงเพื่อใช้การแทนที่กับเนื้อหาขององค์ประกอบ สัญญาณรบกวนเป็นวิธีการปรับใช้การกระจัดด้วยฟังก์ชันสุ่มที่มีโครงสร้างมากขึ้น เสียงรบกวนประเภทหนึ่งคือเสียงรบกวน Perlin (ซึ่งเป็นสิ่งที่feTurbulenceใช้ภายใต้ประทุน) ซึ่งจะมีลักษณะเช่นนี้หากเราแสดงผลลัพธ์ของการเรียกใช้ฟังก์ชันเสียงรบกวน Perlin:

    เอาต์พุตเสียงรบกวน Perlin

    เราใช้ค่าสัญญาณรบกวนสำหรับแต่ละพิกัดในพื้นที่ และใช้ค่านั้นเพื่อค้นหาพิกัดใหม่ใน “ contents” shader

    มาอัปเดต shader ของเราเพื่อใช้ฟังก์ชัน Perlin noise (ดัดแปลงมาจากGithub repo นี้ ) จากนั้นเราจะใช้มันเพื่อกำหนดการแมปพิกัดจากพิกัดอินพุตไปยังพิกัดเอาต์พุต (เช่น แผนที่การกระจัด)

    การใช้ฟังก์ชันเสียงรบกวนนี้ เราได้ผลลัพธ์ที่ดีกว่ามาก! แมงกะพรุนดูราวกับว่ากำลังเคลื่อนไหวอยู่ในน้ำ

    Perlin Noise นำไปใช้กับตัวแมงกะพรุน

    แต่ทำไมฉันถึงใช้สิ่งนี้

    ณ จุดนี้ คุณอาจจะสงสัยว่า Rebecca นี่มันเจ๋งดี แต่เจาะจงมากในกรณีการใช้งานของมัน แน่นอน — บางทีคุณอาจไม่ได้ทำแมงกะพรุนเคลื่อนไหวทุกวันในที่ทำงาน (เราคงฝันไปใช่ไหม?) แต่RenderEffectsสามารถนำไปใช้กับต้นไม้ที่ประกอบได้ - ให้คุณใส่เอฟเฟกต์กับอะไรก็ได้ที่คุณต้องการ

    ตัวอย่างเช่น เหตุใดคุณจึงไม่ต้องการให้ข้อความไล่ระดับสีหรือหน้าจอที่เรียบเรียงได้ทั้งหมดมีเอฟเฟกต์นอยซ์หรือเอฟเฟกต์ AGSL อื่นใดที่ใจคุณต้องการ

    Perlin Noise ใช้กับ Composable ทั้งหมด

    ปิดท้าย

    ดังนั้นเราจึงได้กล่าวถึงแนวคิดที่น่าสนใจมากมายในบล็อกโพสต์นี้ — การสร้างแบบกำหนดเองImageVectorsจาก SVG, การทำให้ส่วนต่างๆ เคลื่อนไหวของ an ImageVectorและการใช้ AGSL shaders RenderEffectsกับ UI ของเราในการเขียน

    สำหรับรหัสทั้งหมดของแมงกะพรุน - ตรวจสอบส่วนสำคัญทั้งหมด ที่นี่ สำหรับข้อมูลเพิ่มเติมเกี่ยวกับAGSL RenderEffects — ดูเอกสารประกอบหรือตัวอย่าง JetLaggedสำหรับตัวอย่างการใช้งานอื่นๆ

    หากคุณมีคำถามใดๆ โปรดติดต่อ Mastodon androiddev.social/@riggarooหรือTwitter

    ขอบคุณJolanda Verhoef , Nick Butcher , Florina Muntenescu , Romain Guy , Nader Jawad สำหรับข้อเสนอแนะที่มีค่าเกี่ยวกับโพสต์นี้