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

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

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

הדגמה

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

סקירה כללית

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

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

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

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

צילום מסך של Chrome DevTools עם החלונית Elements פתוחה, וההגדרה Reduced motion מוגדרת ל-reduce, והתג h1 מוצג ללא פיצול
המשתמש מעדיף תנועה מופחתת: הטקסט קריא / לא מפוצל

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

צילום מסך של Chrome DevTools עם החלונית Elements פתוחה, וההגדרה Reduced motion מוגדרת ל-reduce, והתג h1 מוצג ללא פיצול
User is OK with motion; text split into multiple <span> elements

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

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

הכנת התנאי של שירות ה-CSS

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

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

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

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

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

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

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 מאפשרת לנו לציין אילו תווים לחתוך. עברתי על פני רווח ריק, שמציין שיש הפרדה בין מילים.

יצירת פונקציה בסיסית של תיבות

האפקט דורש תיבות לכל אות, ואפשר לראות בפונקציות האלה שמתבצעת קריאה ל-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;
}

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

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

בדוגמה הזו נעשה שימוש במעברי 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 או לארח הדגמה משלכם, לצייץ לי אותה ואוסיף אותה לקטע 'רמיקסים של הקהילה' שבהמשך.

מקור

הדגמות והשראה נוספות

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