יצירת אנימציות של טקסט מפוצל

סקירה כללית בסיסית של בניית אנימציות של אותיות ומילים.

בפוסט הזה אני רוצה לשתף את הדרך שבה אנו פותרים את הבעיה של אנימציות טקסט מחולקות ואינטראקציות באינטרנט, כך שהן יהיו מינימליסטיות, נגישות ויפעלו בכל הדפדפנים. כדאי לנסות את הדגמה.

דמו

אם אתם מעדיפים סרטון, הנה גרסה של הפוסט הזה ב-YouTube:

סקירה כללית

אנימציות של טקסט מפוצל יכולות להיות מדהימות. אנחנו כמעט לא נצליח לממש את פוטנציאל האנימציה בפוסט הזה, אבל הוא מספק בסיס שאפשר לבנות. המטרה היא ליצור אנימציה פרוגרסיבית. הטקסט צריך להיות קריא כברירת מחדל, והאנימציה צריכה להופיע מעליו. אפקטים של תנועה בטקסט מפוצל יכולים להיות גרנדיוזיים ולגרום להפרעה, לכן נבצע מניפולציות ב-HTML או נחיל סגנונות תנועה רק אם המשתמש מסכים לתנועה.

לפניכם סקירה כללית של תהליך העבודה והתוצאות:

  1. מכינים משתנים מותנים של תנועה מופחתת ל-CSS ול-JS.
  2. הכנה של כלי חלוקת טקסט ב-JavaScript.
  3. תזמור של התנאים והכלים בטעינת הדף.
  4. לכתוב מעברים ואנימציות ב-CSS לאותיות ולמילים (החלק היחסי של ה-CSS).

הנה תצוגה מקדימה של התוצאות התנאייות שאנחנו רוצים לקבל:

צילום מסך של Chrome DevTools עם חלונית הרכיבים פתוחה והתנועה המוקטנת מוגדרת ל'הפחתה', והכותרת h1 מוצגת ללא פיצול
העדפת המשתמש היא צמצום תנועה: הטקסט קריא / לא מפוצל

אם המשתמש מעדיף תנועה מופחתת, אנחנו משאירים את מסמך ה-HTML ללא שינוי ולא מוסיפים אנימציה. אם התנועה תקינה, אנחנו ממשיכים ומקצצים אותה לחתיכות. זוהי תצוגה מקדימה של ה-HTML אחרי ש-JavaScript פיצל את הטקסט לפי אות.

צילום מסך של Chrome DevTools עם חלונית הרכיבים פתוחה והתנועה המוקטנת מוגדרת ל'הפחתה', והכותרת h1 מוצגת ללא פיצול
התנועה מקובלת על המשתמש, הטקסט מפוצל למספר רכיבי <span>

הכנת תנאים לתנועה

נשתמש בשאילתת המדיה @media (prefers-reduced-motion: reduce), שזמינה בנוחות, מ-CSS ומ-JavaScript בפרויקט הזה. שאילתה הווידאו הזו היא התנאי העיקרי שלנו להחלטה אם לפצל טקסט או לא. שאילתת המדיה מסוג CSS תשמש כדי למנוע את ההעברות והאנימציות, ושאילתת המדיה מסוג JavaScript תשמש כדי למנוע את מניפולציית ה-HTML.

הכנת התנאי ב-CSS

השתמשתי ב-PostCSS כדי להפעיל את התחביר של שאילתות מדיה ברמה 5, שבו אפשר לאחסן שאילתת מדיה בוליאנית במשתנה:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

הכנת התנאי ב-JS

ב-JavaScript, הדפדפן מספק דרך לבדוק שאילתות מדיה. השתמשתי בפירוק מבנה כדי לחלץ את התוצאה הבוליאנרית מהבדיקה של שאילתת המדיה ולשנות את השם שלה:

const {matches:motionOK} = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
)

לאחר מכן אוכל לבדוק אם הערך של motionOK הוא 1, ואשנה את המסמך רק אם המשתמש לא ביקש לצמצם את התנועה.

if (motionOK) {
  // document split manipulations
}

אפשר לבדוק את אותו ערך באמצעות PostCSS כדי להפעיל את התחביר @nest מ-Nesting Draft 1. כך אפשר לאחסן במקום אחד את כל הלוגיקה של האנימציה ואת דרישות הסגנון שלה לרכיב ההורה ולרכיבי הצאצאים:

letter-animation {
  @media (--motionOK) {
    /* animation styles */
  }
}

עם המאפיין המותאם אישית של PostCSS ועם ערך בוליאני של JavaScript, אנחנו מוכנים לשדרג את האפקט באופן מותנה. זה מביא אותנו לקטע הבא, שבו אסביר על הקוד ב-JavaScript שממיר מחרוזות לרכיבים.

פיצול טקסט

אי אפשר להוסיף אנימציה בנפרד לאותיות, למילים, לשורות וכו' בטקסט באמצעות CSS או JS. כדי ליצור את האפקט, אנחנו צריכים תיבות. אם רוצים להוסיף אנימציה לכל אות, כל אות צריכה להיות רכיב. אם רוצים להוסיף אנימציה לכל מילה, כל מילה צריכה להיות רכיב.

  1. יצירת פונקציות שירות של JavaScript לפיצול מחרוזות לרכיבים
  2. תזמור השימוש בכלים האלה

פונקציית השירות לפיצול אותיות

מקום טוב להתחיל בו הוא פונקציה שמקבלת מחרוזת ומחזירה כל אות במערך.

export const byLetter = text =>
  [...text].map(span)

התחביר של spread מ-ES6 עזר מאוד לבצע את המשימה הזו במהירות.

פונקציית השירות לפיצול מילים

בדומה לפונקציה של פיצול אותיות, הפונקציה הזו מקבלת מחרוזת ומחזירה כל מילה במערך.

export const byWord = text =>
  text.split(' ').map(span)

השיטה split() במחרוזות JavaScript מאפשרת לנו לציין אילו תווים לחתוך. עברתי על רווח ריק, שמציין חלוקה בין מילים.

פונקציית השירות של boxes

באפקט הזה נדרשות תיבות לכל אות, ואנחנו רואים בפונקציות האלה קריאה ל-map() באמצעות הפונקציה span(). זוהי הפונקציה span().

const span = (text, index) => {
  const node = document.createElement('span')

  node.textContent = text
  node.style.setProperty('--index', index)

  return node
}

חשוב לציין שמוגדר מאפיין מותאם אישית בשם --index עם המיקום במערך. זה נהדר שיש תיבות לאנימציות של האותיות, אבל הוספה של אינדקס ב-CSS היא תוספת קטנה לכאורה שיש לה השפעה גדולה. ההשפעה הגדולה ביותר היא הגדלה. נוכל להשתמש ב---index כדי לבצע הזזה של אנימציות כדי ליצור מראה מדורג.

סיכום של כלי השירות

המודול splitting.js הושלם:

const span = (text, index) => {
  const node = document.createElement('span')

  node.textContent = text
  node.style.setProperty('--index', index)

  return node
}

export const byLetter = text =>
  [...text].map(span)

export const byWord = text =>
  text.split(' ').map(span)

בשלב הבא נסביר איך לייבא את הפונקציות byLetter() ו-byWord() ולהשתמש בהן.

תזמור פיצול

השילוב של כל הכלים האלה מאפשר:

  1. איך מאתרים את הרכיבים שצריך לפצל
  2. פיצול שלהן והחלפת טקסט ב-HTML

לאחר מכן, שירות ה-CSS ייקח את המושכות ויוסיף אנימציה לרכיבים או לתיבות.

איתור רכיבים

בחרתי להשתמש במאפיינים ובערכים כדי לאחסן מידע על האנימציה הרצויה ועל אופן הפיצול של הטקסט. אהבתי להוסיף את האפשרויות האלה ל-HTML. המאפיין split-by משמש מ-JavaScript כדי למצוא רכיבים וליצור תיבות לאותיות או למילים. המאפיין letter-animation או word-animation משמש ב-CSS כדי לטרגט צאצאים של רכיבים ולהחיל טרנספורמציות ואנימציות.

לפניכם דוגמה ל-HTML שממחישה את שני המאפיינים:

<h1 split-by="letter" letter-animation="breath">animated letters</h1>
<h1 split-by="word" word-animation="trampoline">hover the words</h1>

חיפוש רכיבים מ-JavaScript

השתמשתי בתחביר של הסלקטור ב-CSS בשביל נוכחות מאפיינים כדי לאסוף את רשימת הרכיבים שרוצים לפצל את הטקסט שלהם:

const splitTargets = document.querySelectorAll('[split-by]')

חיפוש רכיבים מ-CSS

בנוסף, השתמשתי בסלקטור של נוכחות המאפיין ב-CSS כדי לתת לכל האנימציות של האותיות את אותם סגנונות בסיס. לאחר מכן נשתמש בערך המאפיין כדי להוסיף סגנונות ספציפיים יותר להשגת אפקט.

letter-animation {
  @media (--motionOK) {
    /* animation styles */
  }
}

פיצול טקסט במקום

לכל אחד מהיעדים המפוצלים שאנחנו מוצאים ב-JavaScript, נחלק את הטקסט שלהם על סמך הערך של המאפיין ונמפה כל מחרוזת ל-<span>. לאחר מכן אנחנו יכולים להחליף את הטקסט של הרכיב בתיבות שיצרנו:

splitTargets.forEach(node => {
  const type = node.getAttribute('split-by')
  let nodes = null

  if (type === 'letter') {
    nodes = byLetter(node.innerText)
  }
  else if (type === 'word') {
    nodes = byWord(node.innerText)
  }

  if (nodes) {
    node.firstChild.replaceWith(...nodes)
  }
})

סיכום תזמור

index.js בסיום:

import {byLetter, byWord} from './splitting.js'

const {matches:motionOK} = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
)

if (motionOK) {
  const splitTargets = document.querySelectorAll('[split-by]')

  splitTargets.forEach(node => {
    const type = node.getAttribute('split-by')
    let nodes = null

    if (type === 'letter')
      nodes = byLetter(node.innerText)
    else if (type === 'word')
      nodes = byWord(node.innerText)

    if (nodes)
      node.firstChild.replaceWith(...nodes)
  })
}

אפשר לקרוא את הקוד ב-JavaScript באנגלית:

  1. מייבאים כמה פונקציות עזר.
  2. כדאי לבדוק אם התנועה מתאימה למשתמש הזה ואם לא לעשות כלום.
  3. לכל רכיב שרוצים לפצל
    1. כדאי לפצל אותן לפי האופן שבו הן יפוצלו.
    2. מחליפים טקסט ברכיבים.

פיצול אנימציות ומעברים

מניפולציית המסמך שמפוצל למעלה פותחת לכם צוהר למגוון אנימציות ואפקטים פוטנציאליים באמצעות CSS או JavaScript. בסוף המאמר יש כמה קישורים שיעזרו לך לתת השראה לפיצול שלך.

הגיע הזמן להראות מה אפשר לעשות עם זה! אשתף 4 אנימציות ומעברים מבוססי CSS. 🤓

פיצול אותיות

כבסיס לאפקטים של אותיות מפוצלות, מצאתי שהקוד הבא ב-CSS עוזר. הוספתי את כל המעברים והאנימציות מאחורי שאילתת המדיה של התנועה, ולאחר מכן הוספתי לכל אות צאצא חדשה span מאפיין תצוגה וגם סגנון של מה לעשות עם רווחים לבנים:

[letter-animation] > span {
  display: inline-block;
  white-space: break-spaces;
}

סגנון הרווחים הלבנים חשוב כדי שמנוע הפריסה לא יאוחס את ה-spans שהם רק רווח. עכשיו נעבור לדברים המעניינים בנושא מצב סטטי.

דוגמה לאותיות מפוצלות למעבר

בדוגמה הזו נעשה שימוש במעברי CSS כדי ליצור את אפקט הטקסט המפוצל. כדי ליצור מעברים, אנחנו צריכים מצבים שהמנוע יבצע אנימציה ביניהם. בחרתי שלושה מצבים: ללא מעברי עכבר, מעברי עכבר במשפט ומעברי עכבר על אות.

כשהמשתמש מעביר את העכבר מעל המשפט, כלומר מעל המאגר, אני מקטין את כל הצאצאים כאילו המשתמש דחף אותם רחוק יותר. לאחר מכן, כשהמשתמש מרחף עם אות, אני מעביר אותה הלאה.

@media (--motionOK) {
  [letter-animation="hover"] {
    &:hover > span {
      transform: scale(.75);
    }

    & > span {
      transition: transform .3s ease;
      cursor: pointer;

      &:hover {
        transform: scale(1.25);
      }
    }
  }
}

דוגמה להוספת אנימציה לאותיות מפוצלות

בדוגמה הזו נעשה שימוש באנימציה מוגדרת מראש של @keyframe כדי להציג אנימציה של כל אות ללא הגבלת זמן, תוך ניצול של מדד הנכס המותאם אישית בשורה כדי ליצור אפקט של תנועה רציפה.

@media (--motionOK) {
  [letter-animation="breath"] > span {
    animation:
      breath 1200ms ease
      calc(var(--index) * 100 * 1ms)
      infinite alternate;
  }
}

@keyframes breath {
  from {
    animation-timing-function: ease-out;
  }
  to {
    transform: translateY(-5px) scale(1.25);
    text-shadow: 0 0 25px var(--glow-color);
    animation-timing-function: ease-in-out;
  }
}

פיצול מילים

בדוגמאות האלה, Flexbox עבד אצלי כסוג של קונטיינר, והשתמשתי ביחידה ch כרווח ביניהם.

word-animation {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 1ch;
}
כלי הפיתוח של Flexbox שמוצגת בו הרווח בין המילים

דוגמה למילים מפוצלות במעבר

בדוגמת המעבר הזו, אני משתמש שוב כשמעבירים את העכבר מעליו. מכיוון שהאפקט מסתיר את התוכן בהתחלה עד שמעבירים מעליו את העכבר, הקפדתי שהאינטראקציה והסגנונות יחולו רק אם המכשיר מסוגל להעביר מעליו את העכבר.

@media (hover) {
  [word-animation="hover"] {
    overflow: hidden;
    overflow: clip;

    & > span {
      transition: transform .3s ease;
      cursor: pointer;

      &:not(:hover) {
        transform: translateY(50%);
      }
    }
  }
}

דוגמה להוספת אנימציה למילים מפוצלות

בדוגמה הזו של אנימציה, שוב משתמשים ב-CSS @keyframes כדי ליצור אנימציה אינסופית בשלבים בפסקה רגילה של טקסט.

[word-animation="trampoline"] > span {
  display: inline-block;
  transform: translateY(100%);
  animation:
    trampoline 3s ease
    calc(var(--index) * 150 * 1ms)
    infinite alternate;
}

@keyframes trampoline {
  0% {
    transform: translateY(100%);
    animation-timing-function: ease-out;
  }
  50% {
    transform: translateY(0);
    animation-timing-function: ease-in;
  }
}

סיכום

עכשיו אתה יודע איך עשיתי את זה, איך היית?! 🙂

נרחיב את הגישות שלנו ונלמד את כל הדרכים לפיתוח באינטרנט. אפשר ליצור Codepen או לארח הדגמה משלכם, לשלוח לי הודעה ב-Twitter ואני אוסיף אותו לקטע 'רמיקסים לקהילה' בהמשך.

מקור

עוד הדגמות והשראה

רמיקסים של הקהילה