वेब पर एक्सेसिबल और तेज़ AI अवतार एनिमेशन
Charmotion टीम द्वारा · अपडेट:

बातचीत के बगल में लूप होने वाले AI avatar के लिए WCAG 2.2.2 के तहत pause करने या शांत होने का तरीका चाहिए, prefers-reduced-motion सेट होने पर एक स्थिर pose, और screen readers के लिए text status। स्पीड के लिए एक ही Rive instance चलाएँ, offscreen होने या tab छिपने पर उसे pause करें, .riv को cache करें और canvas को mounted रखें।
मुख्य बातें
- दूसरे content के बगल में अपने आप शुरू होने वाले और पाँच सेकंड से लंबे मोशन के लिए pause, stop या hide करने का तरीका होना चाहिए।
- मोशन कम करें सेटिंग चालू होने पर मौजूदा स्टेट की एक स्थिर इमेज दिखाएँ और उसका मतलब text में रखें।
- IntersectionObserver और Page Visibility API से Rive instance को pause करें; Rive offscreen graphics को pause करने की सलाह देता है।
- Canvas को assistive tech से छिपाएँ और सिर्फ़ अर्थपूर्ण बदलावों को एक polite status region में announce करें।
पहले से कोई कैरेक्टर या लोगो है? अपने कैरेक्टर को असली ऐप स्क्रीन में देखें: स्प्लैश, ऑनबोर्डिंग, लोडिंग और एम्प्टी स्टेट स्क्रीन। यह फ़्री है, आपके ब्राउज़र में चलता है और कुछ भी अपलोड नहीं होता।
क्या हमेशा लूप होने वाला avatar WCAG 2.2.2 का उल्लंघन करता है?
हो सकता है। WCAG 2.2 का Success Criterion 2.2.2 (Pause, Stop, Hide, Level A) ऐसे moving content पर लागू होता है जो अपने आप शुरू होता है, पाँच सेकंड से ज़्यादा चलता है और दूसरे content के साथ-साथ दिखाया जाता है। ऐसे content के लिए यूज़र के पास उसे pause, stop या hide करने का तरीका होना चाहिए (W3C)। पूरे session में chat के बगल में चलता idle लूप ये तीनों शर्तें पूरी करता है।
नियम का पालन करने के तरीके, सबसे आसान से सबसे लचीले तक:
- मोशन को activity से जोड़ें। जब assistant सुन रहा हो, सोच रहा हो या बोल रहा हो, तब एनिमेट करें, और बातचीत शांत होने पर कुछ सेकंड में स्थिर pose पर आ जाएँ। पाँच सेकंड के अंदर रुकने वाला मोशन इस criterion के दायरे से बाहर है; लगातार चलता रहने वाला idle लूप बाहर नहीं है।
- एक control दें। Avatar के पास "एनिमेशन pause करें" toggle, या static आइकन दिखाने की सेटिंग, सीधे इस criterion को पूरा करती है और लंबे जवाबों को भी कवर करती है।
- इसे छिपाएँ। यूज़र्स को assistant panel collapse करने या avatar बंद करने देना भी एक मान्य तरीका है।
Charmotion files में एक sleeping मूड (8) होता है जो pause करने से पहले कम मोशन वाले आराम के स्टेट के रूप में अच्छा काम करता है: कैरेक्टर साफ़ तौर पर धीमा होता जाता है, और उस पर pause करना जानबूझकर किया गया लगता है। Charmotion इसे, बाकी नौ मूड और छह रिएक्शन के साथ, आपके अपने कैरेक्टर या लोगो से बनाता है।
Avatar को prefers-reduced-motion पर कैसे प्रतिक्रिया देनी चाहिए?
prefers-reduced-motion: reduce बताता है कि यूज़र ने अपने device से गैर-ज़रूरी मोशन कम करने को कहा है (MDN)। Avatar के ambient लूप परिभाषा से ही गैर-ज़रूरी हैं। पर उनका मतलब, जैसे "सोच रहा है" या "सुन रहा है", ज़रूरी है, इसलिए वह मतलब text में ले जाएँ।
"use client";
import { useEffect, useState } from "react";
export function usePrefersReducedMotion(): boolean {
const [reduce, setReduce] = useState(false);
useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduce(mq.matches);
const onChange = () => setReduce(mq.matches);
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}, []);
return reduce;
}
export function Avatar({ mood, label }: { mood: number; label: string }) {
const reduce = usePrefersReducedMotion();
return (
<div className="flex items-center gap-2">
{reduce
? <img src="/avatar-still.png" alt="" width={64} height={64} />
: <LiveAvatar mood={mood} />}
<span role="status">{label}</span>
</div>
);
}
स्थिर image render करना सबसे भरोसेमंद विकल्प है, और इससे runtime download भी बच जाता है। Live एनिमेशन को बस किसी भी फ़्रेम पर pause न करें; बीच मोशन में रुका लूप किसी अटपटे pose पर अटक सकता है। साफ़ idle फ़्रेम से बनी still image इस्तेमाल करें। अगर आप चाहते हैं कि स्टेट दिखता रहे, तो एनिमेट करने के बजाय कुछ stills (idle, listening, talking) के बीच स्विच करें।
Hook बदलावों को सुनता है, इसलिए जो यूज़र session के बीच सेटिंग बदलते हैं, उन्हें reload के बिना सही व्यवहार मिलता है।
Avatar के offscreen होने या tab छिपने पर उसे pause कैसे करें?
Rive की best practices कहती हैं कि अगर कोई graphic scroll होकर offscreen चला जाए और उसे चलते रहने की ज़रूरत न हो, तो उसका pause API call करें (Rive best practices)। पेज पर visibility के लिए IntersectionObserver और background tabs के लिए Page Visibility API को साथ इस्तेमाल करें (MDN, MDN):
"use client";
import { useEffect, useRef } from "react";
import { useRive } from "@rive-app/react-webgl2";
export function LiveAvatar() {
const box = useRef<HTMLDivElement>(null);
const { rive, RiveComponent } = useRive({
src: "/character.riv",
stateMachine: "CharacterViewModelMachine",
autoplay: true,
autoBind: true,
});
useEffect(() => {
const el = box.current;
if (!rive || !el) return;
let onScreen = true;
const sync = () => {
if (onScreen && document.visibilityState === "visible") rive.play();
else rive.pause();
};
const io = new IntersectionObserver(([entry]) => {
onScreen = entry.isIntersecting;
sync();
});
io.observe(el);
document.addEventListener("visibilitychange", sync);
return () => {
io.disconnect();
document.removeEventListener("visibilitychange", sync);
};
}, [rive]);
return (
<div ref={box} aria-hidden="true" style={{ width: 96, height: 96 }}>
<RiveComponent />
</div>
);
}
Inactivity pause के लिए भी यही sync function इस्तेमाल करें: "asleep" के लिए एक flag जोड़ें और उसे condition में शामिल करें। नया message आने पर flag हटाएँ और अगला मूड सेट करने से पहले sync() call करें।
Rive canvases के बीच एक ही WebGL context क्यों शेयर करें?
Browsers सीमित करते हैं कि एक पेज एक साथ कितने WebGL contexts रख सकता है, और कई contexts बनाना महँगा पड़ता है। Rive React runtime का useOffscreenRenderer parameter, जो डिफ़ॉल्ट रूप से true होता है, हर Rive instance को एक ही offscreen WebGL context शेयर करवाता है (या कोई न हो तो उसे बनाता है), और इसी से एक स्क्रीन पर कई Rive एनिमेशन चल पाते हैं (Rive React parameters)। इसे चालू रहने दें।
| जाँच का बिंदु | क्यों | कैसे |
|---|---|---|
| लंबे लूप के लिए pause/stop/hide | WCAG 2.2.2 | sleeping पर शांत करें, फिर pause करें; या toggle दें |
| मोशन कम करें | यूज़र की पसंद | स्थिर इमेज और text status |
| Offscreen और छिपे tabs में pause करें | Rive की best practice; CPU और battery बचती है | IntersectionObserver + visibilitychange |
| शेयर किया गया WebGL context | Context की सीमाएँ | useOffscreenRenderer को डिफ़ॉल्ट पर रखें |
| एक live instance | हर instance हर फ़्रेम render करता है | मौजूदा turn के लिए live avatar, history के लिए stills |
| .riv को cache करें | एक बार parse और decode करें | बार-बार के instances के लिए लोड की गई फ़ाइल दोबारा इस्तेमाल करें |
| स्थिर canvas | Remount से एनिमेशन फिर से शुरू होता है | useRive और RiveComponent को साथ रखें |
शेयर किया गया context सीमाओं में मदद करता है, पर हर instance जितने फ़्रेम चलता है, हर फ़्रेम में rendering का समय लेता है। जो chat हर message के बगल में live avatar दिखाती है, वह इस खर्च को बातचीत की लंबाई से गुणा कर देती है। एक live avatar, और पुराने messages के बगल में static image, सस्ता पड़ता है और ज़्यादा शांत दिखता है। अगर आपको सच में एक ही फ़ाइल कई जगह चाहिए, तो Rive .riv को cache करने की सलाह देता है ताकि वह सिर्फ़ एक बार parse और decode हो (Rive best practices)।
React को एनिमेशन फिर से शुरू करने से कैसे रोकें?
Rive की React guidance साफ़ कहती है: अगर React बार-बार canvas को unmount या दोबारा create करता है, तो एनिमेशन फिर से शुरू हो सकता है, गायब हो सकता है या दिखना बंद हो सकता है (Rive React best practices)। Chat interfaces में यह खास तौर पर होता है, क्योंकि वे हर streamed token पर re-render होते हैं।
useRiveऔरRiveComponentको एक ही component में रखें। Instance को parent में बनाकर canvas को child में render न करें।- Avatar को एक बार mount करें, किसी स्थिर जगह पर, जैसे chat header में, न कि re-render होने वाली list के अंदर।
- बदलती keys से बचें। मौजूदा message id से जुड़ी
keyहर turn पर canvas को remount कर देती है। - इसे values से चलाएँ, नए components से नहीं। मूड व्यू मॉडल के ज़रिए बदलें; हर स्टेट के लिए एक avatar component को दूसरे से न बदलें।
- छोटे स्टेट के लिए इसे conditional rendering से toggle न करें। CSS से छिपाने या pause करने पर लोड की गई फ़ाइल और position बनी रहती है।
streaming chat avatar गाइड में इसी तरह बना एक पूरा component दिखाया गया है।
स्क्रीन रीडर को क्या सुनाई देना चाहिए?
कैनवस अपने आप में स्क्रीन रीडर को कुछ नहीं बताता, और उसे ऐसा करने की कोशिश भी नहीं करनी चाहिए। ऊपर के उदाहरणों की तरह अवतार के कंटेनर पर aria-hidden="true" लगाएँ, ताकि असिस्टिव टेक्नोलॉजी उसे छोड़ दे।
इसके बजाय जानकारी टेक्स्ट में दें:
- एक दिखने वाला स्टेटस लेबल, जैसे "सुन रहा है", "सोच रहा है" या "बोल रहा है", अवतार या इनपुट के पास। इससे उन यूज़र्स को भी मदद मिलती है जो स्क्रीन देख तो रहे हैं, पर अवतार पर ध्यान नहीं दे रहे।
- उस लेबल के लिए एक polite लाइव रीजन (
role="status"), ताकि बदलाव बिना रुकावट के अनाउंस हों। - सिर्फ़ अहम बदलाव अनाउंस करें। तेज़ जवाबों में सोचने वाला स्टेट एक सेकंड से भी कम समय तक रह सकता है। हर ट्रांज़िशन अनाउंस करने से शोर बढ़ता है; थोड़ी देर के बाद ही "सोच रहा है" अनाउंस करना, और जवाब आने पर खुद जवाब अनाउंस करना, आमतौर पर काफ़ी होता है।
- एनिमेशन का वर्णन न करें। "रोबोट हाथ हिला रहा है" से कोई जानकारी नहीं मिलती। "कनेक्ट हो गया" से मिलती है।
एरर रिएक्शन पर खास ध्यान दें: रिएक्शन सिर्फ़ सजावट है, लेकिन एरर एक जानकारी है। इसे हमेशा एक टेक्स्ट मैसेज के साथ दिखाएँ, और चमकने वाले इफ़ेक्ट से बचें। WCAG के व्यापक नियमों के लिए एक्सेसिबल एनिमेशन और मोशन कम करें देखें।

AI अवतार को एक्सेसिबल और कुशल बनाएँ
- कैनवस को असिस्टिव टेक से छिपाएँ. Rive कैनवस को
aria-hidden="true"वाले कंटेनर में रखें और टेक्स्ट स्टेटस कोrole="status"एलिमेंट में दिखाएँ। - मोशन कम करें सेटिंग का ध्यान रखें.
prefers-reduced-motionचेक करें, और जब यहreduceपर सेट हो, तो लाइव अवतार की जगह idle पोज़ की एक स्थिर इमेज दिखाएँ। - न दिखने पर पॉज़ करें. जब IntersectionObserver बताए कि अवतार स्क्रीन से बाहर है या पेज छिप गया है, तब
rive.pause()कॉल करें, और वापस आने परrive.play()कॉल करें। - कुछ देर गतिविधि न होने पर स्थिर करें. कुछ समय तक बातचीत न होने पर sleeping या idle स्टेट पर स्विच करें, फिर अगले मैसेज तक रेंडरिंग रोक दें।
- एक ही स्थिर इंस्टेंस रखें. मैसेज लिस्ट के बाहर सिर्फ़ एक लाइव अवतार माउंट करें,
useRiveऔरRiveComponentको साथ रखें, औरuseOffscreenRendererको चालू रहने दें।
अक्सर पूछे जाने वाले सवाल
क्या एनिमेटेड चैटबॉट अवतार के लिए पॉज़ बटन ज़रूरी है?
अगर यह अपने आप शुरू होता है और दूसरे कंटेंट के साथ पाँच सेकंड से ज़्यादा लूप करता है, तो WCAG 2.2.2 के अनुसार इसे पॉज़ करने, रोकने या छिपाने का तरीका होना चाहिए। एक टॉगल काफ़ी है। बातचीत शांत होने के कुछ सेकंड के अंदर स्थिर पोज़ में आ जाने से भी idle मोशन छोटा रहता है, लेकिन लंबे talking लूप के लिए फिर भी एक कंट्रोल देना बेहतर है।
Rive अवतार स्क्रॉल होकर स्क्रीन से बाहर चला जाए, तो क्या उसे पॉज़ करना चाहिए?
हाँ। Rive की best practices में सुझाया गया है कि जब कोई ग्राफ़िक स्क्रीन से बाहर हो और उसे चलते रहने की ज़रूरत न हो, तो pause कॉल करें। दिखने का पता लगाने के लिए IntersectionObserver और बैकग्राउंड टैब के लिए Page Visibility API इस्तेमाल करें, फिर बदलाव के हिसाब से rive.pause() और rive.play() कॉल करें।
मोशन कम करें सेटिंग में अवतार को क्या दिखाना चाहिए?
किसी साफ़ पोज़ की एक स्थिर इमेज, आमतौर पर idle, और मौजूदा स्टेट टेक्स्ट के रूप में, जैसे "सुन रहा है" या "सोच रहा है"। लाइव एनिमेशन को किसी भी मनमाने फ़्रेम पर पॉज़ करने से बचें। अगर स्टेट दिखाना ज़रूरी है, तो उनके बीच एनिमेट करने के बजाय कुछ स्टैटिक इमेज बदलें।
क्या एक पेज पर कई Rive अवतार दिखा सकते हैं?
हाँ। React रनटाइम डिफ़ॉल्ट रूप से useOffscreenRenderer के ज़रिए सभी इंस्टेंस में एक ही offscreen WebGL context शेयर करता है, जिससे ब्राउज़र की context लिमिट से बचा जा सकता है। फिर भी हर चलता हुआ इंस्टेंस रेंडरिंग का समय लेता है, इसलिए चैट में सिर्फ़ मौजूदा अवतार को एनिमेट करें और पुराने मैसेज के लिए स्टैटिक इमेज इस्तेमाल करें।
इस गाइड से झटपट जवाब
स्रोत
- Understanding SC 2.2.2: Pause, Stop, Hide, W3C WAI
- prefers-reduced-motion, MDN Web Docs
- Best practices, Rive
- React runtime: Parameters and return values, Rive
- React runtime: Best practices, Rive
- Intersection Observer API, MDN Web Docs
- Page Visibility API, MDN Web Docs