Microinteracțiunile ghidează un utilizator prin aplicația dvs. Acestea vă consolidează experiența de utilizare și vă oferă încântare.

Este posibil să fi văzut câteva dintre exemplele slabe de microinteracțiuni pe Dribla sau CodePen. Dar știi cum să-ți construiești propria bibliotecă de widget-uri UI similare?

În acest articol, mă voi concentra pe microinteracțiuni animate folosind Reacţiona, Cadrul de interfață populară Facebook, orientat spre componente. Voi crea trei interacțiuni pentru o casetă de căutare:

  • deschideți și închideți caseta de text
  • mutați în partea de sus a ecranului
  • shake (indicând o eroare)
Cum sa construiesti microinteractiuni animate in React

Voi folosi câteva implementări diferite:

Iată un demo live și codul care îl alimentează.

Aceasta este una dintre mai multe postări despre Higher Order (HOC) și Componente funcționale fără stat. Primul post este despre reutilizarea codului în React și React Native, prin intermediul acestor tehnici.

Ce este o microinteracțiune?

Dan Saffer (care a scris cartea) ne oferă acest lucru definiție: „Microinteracțiunile sunt momente de produs conținute care se învârt în jurul unui singur caz de utilizare – au o sarcină principală.”

Exemplele ar putea fi mai clare. Unele microinteracțiuni sunt peste tot, cum ar fi o schimbare a cursorului când treceți peste un link sau vibrația telefonului dvs. când treceți în modul silențios. Altele, cum ar fi un articol adăugat într-un coș de cumpărături, nu sunt atât de obișnuite (încă).

De ce ar trebui să-mi pese de microinteracțiuni?

Microinteracțiunile pot oferi feedback și pot face aplicația dvs. memorabilă. Când utilizatorii au atât de multe opțiuni de aplicație, microinteracțiuni mai bune ar putea fi capcana de șoareci mai bună pe care ar trebui să o construiți.

Dar nu sunt un designer UX. Așa că vă sugerez să citiți Nick Nick post despre microinteracțiuni.

Noțiuni de bază

Voi folosi creați-reacționați-aplicație pentru a porni o aplicație React, dar orice metodă de configurare React va funcționa. De asemenea, îmi place Material-UI, așa că voi importa și asta. (Această alegere este arbitrară – puteți utiliza o altă bibliotecă widget sau puteți stiliza manual elementele dvs.)

create-react-app search-box-animation
cd search-box-animation
npm install --save material-ui react-tap-event-plugin

Voi crea o simplă casetă de căutare. Acesta va cuprinde două elemente: un buton pictogramă de căutare și o casetă de text. Voi crea o componentă funcțională apatridă pentru caseta de căutare. (Componentele funcționale fără stare sunt funcții care redau componentele React și nu mențin starea, adică utilizarea setState. Puteți afla mai multe în acest sens tutorial sau precedentul meu post.)

SearchBox.js

import React from 'react';
import {TextField, IconButton} from 'material-ui'
import SearchIcon from 'material-ui/svg-icons/action/search';
const SearchBox = ({isOpen, onClick}) => {
    const baseStyles = {
        open: {
            width: 300,
        },
        closed: {
            width: 0,
        },
        smallIcon: {
            width: 30,
            height: 30
        },
        icon: {
            width: 40,
            height: 40,
            padding: 5,
            top: 10
        },
        frame: {
            border: 'solid 1px black',
            borderRadius: 5
        }
    };
const textStyle = isOpen ? baseStyles.open : baseStyles.closed;
const divStyle = Object.assign({}, textStyle, baseStyles.frame);
    divStyle.width += baseStyles.icon.width + 5;
return (
        <div style={divStyle}>
            <IconButton iconStyle={baseStyles.smallIcon} style={baseStyles.icon} onClick={() => onClick()}>
                <SearchIcon />
            </IconButton>
            <TextField name="search" style={textStyle}/>
        </div>
    );
};
export  default SearchBox;

(Voi folosi onClick sună mai târziu.)

isOpen prop setează SearchBox redare deschisă sau închisă.

Cum sa construiesti microinteractiuni animate in React
isOpen = adevărat / isOpen = fals

Utilizarea componentelor de ordin superior pentru a separa problemele

Aș putea să mă schimb SearchBox la o componentă obișnuită și adăugați cod care ar deschide și închide caseta de text atunci când dați clic, de exemplu.

Dar prefer să separ animația de scopul principal al casetei de căutare. Caseta de căutare afișează / captează o valoare a interogării și trimite această interogare către un alt controler. Aceasta este o decizie subiectivă de proiectare, dar are beneficii practice: pot reutiliza logica microinteracțiunii cu o altă componentă de intrare a utilizatorului.

Componente de ordin superior (HOC) sunt funcții care returnează o componentă nouă. Această componentă împachetează o componentă și adaugă funcționalitate. Voi crea un HOC pentru a adăuga comportamentul de deschidere / închidere la SearchBox.

Crea expanding-animation.js

import React, {Component} from 'react';
const makeExpanding = (Target) => {
    return class extends Component {
        constructor(props) {
            super(props);
            this.state = {isOpen: false};
        }

        onClick = () => {
            this.setState({isOpen: !this.state.isOpen});
        };

        render() {
            return (
                <Target {...this.props}
                        isOpen={this.state.isOpen}
                        onClick={this.onClick}
                />
            );
        }
    }
};
export default makeExpanding;

Actualizați App.js după cum urmează:

import React, {Component} from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';

// (Make material-ui happy)
// Needed for onTouchTap
// http://stackoverflow.com/a/34015469/988941
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();

import SearchBox from './SearchBox'
import makeExpanding from './expanding-animation';

const ExpandingSearchBox = makeExpanding(SearchBox);

class App extends Component {
    render() {
        //https://css-tricks.com/quick-css-trick-how-to-center-an-object-exactly-in-the-center/
        const style = {
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
        };

        return (
            <MuiThemeProvider>
                <div style={style}>
                    <ExpandingSearchBox/>
                </div>
            </MuiThemeProvider>
        );
    }
}
export default App;

Dacă alergi npm start, veți avea o pictogramă de căutare pe care puteți face clic pentru a deschide și a închide caseta de text.

Funcționează, dar deschiderea și închiderea sunt discordante. O animație poate netezi efectul.

Animații

Există trei abordări generale ale animațiilor.

  1. Tranziții CSS
  2. Animații CSS
  3. redarea rapidă și repetată a unui element pentru a simula mișcarea (încadrarea manuală a tastelor)

Tranziții CSS modificați o valoare a proprietății (cum ar fi lățimea) pe o anumită durată de timp. Schimbarea nu trebuie să fie liniară; puteți specifica funcții pentru schimbarea valorilor.

Animații CSS schimbați stilul unui element (cum ar fi dimensiunea, culoarea și poziția). Fiecare stil incremental este un cadru cheie. Creați o serie de cadre cheie pentru a obține efectul dorit.

Ambele tactici CSS redau în mod repetat elemente pentru a simula mișcarea. Puteți face calculele singur, adică opțiunea (3). Mai multe cadre de animație Javascript folosesc această abordare, gestionând calculele. (Voi folosi reacția-mișcare într-un exemplu ulterior.)

Voi folosi toate aceste tehnici în exemplele de mai jos, dar voi începe cu tranzițiile CSS.

Animația extinsă a casetei de text are nevoie de o proprietate CSS: transition

Schimbare expanding-animation.js după cum urmează,

import React, {Component} from 'react';
const animationStyle = {
    transition: 'width 0.75s cubic-bezier(0.000, 0.795, 0.000, 1.000)'
};
const makeExpanding = (Target) => {
    return class extends Component {
        constructor(props) {
            super(props);
            this.state = {isOpen: false};
        }

        onClick = () => {
            this.setState({isOpen: !this.state.isOpen});
        };

        render() {
            return (
                <Target {...this.props}
                        isOpen={this.state.isOpen}
                        onClick={this.onClick}
                        additionalStyles={{text: animationStyle, frame: animationStyle}}/>
            );
        }
    }
};
export default makeExpanding;

Privind schimbarea din linia 21, additionalStyles, SearchBox va îmbina acest stil cu stilurile sale existente în rândurile 29 și 31 de mai jos. (Voi reveni la proprietatea CSS de tranziție în linia 2 într-un moment.)

Actualizați SearchBox.js

import React from 'react';
import {TextField, IconButton} from 'material-ui'
import SearchIcon from 'material-ui/svg-icons/action/search';
const SearchBox = ({isOpen, onClick, additionalStyles}) => {
    const baseStyles = {
        open: {
            width: 300,
        },
        closed: {
            width: 0,
        },
        smallIcon: {
            width: 30,
            height: 30
        },
        icon: {
            width: 40,
            height: 40,
            padding: 5,
            top: 10
        },
        frame: {
            border: 'solid 1px black',
            borderRadius: 5
        }
    };
    
    let textStyle = isOpen ? baseStyles.open : baseStyles.closed;
    textStyle = Object.assign(textStyle, additionalStyles ? additionalStyles.text : {});
    
    const divStyle = Object.assign({}, textStyle, baseStyles.frame, additionalStyles ? additionalStyles.frame : {});
    divStyle.width += baseStyles.icon.width + 5;
    
    return (
        <div style={divStyle}>
            <IconButton iconStyle={baseStyles.smallIcon} style={baseStyles.icon} onClick={() => onClick()}>
                <SearchIcon />
            </IconButton>
            <TextField name="search" style={textStyle}/>
        </div>
    );
};
export  default SearchBox;

Odată cu combinarea stilurilor, animația va intra în vigoare.

1611452710 909 Cum sa construiesti microinteractiuni animate in React
Tranziție CSS: lățime

Rezultatul este o extindere lină a lățimii casetei de text, oferind aspectul pe care îl deschide. CSS transition proprietatea controlează acest lucru (de la linia 2 în expanding-animation.js).

transition: 'width 0.75s cubic-bezier(0.000, 0.795, 0.000, 1.000)'

Vă încurajez să citiți documentație pentru proprietatea de tranziție CSS, deoarece există o varietate de opțiuni. În exemplu, există trei parametri:

  1. proprietate de schimbat: width
  2. durata tranziției: 0.75s
  3. funcție pentru a controla sincronizarea: cubic-bezier(0.000, 0.795, 0.000, 1.000)’

În timp ce eu am ales cubic-bezier ca funcție, linear sau ease sunt printre alte opțiuni. Există instrumente interactive care vă ajută să selectați aceste valori, cum ar fi aceasta constructor cubic-bezier.

Verificați următoarea animație de concept pe care am găsit-o pe Dribble:

1611452710 524 Cum sa construiesti microinteractiuni animate in React
https://dribbble.com/shots/2751256-Google-Search

Există mai multe elemente în interacțiune; dar aș vrea să mă concentrez asupra mișcării casetei de căutare către partea de sus a ecranului.

Îmi pot muta umila casetă de căutare cu o tranziție CSS. Creați un nou HOC, move-up-animation.js


import React, {Component} from 'react';
const animationStyle = {
    transform: 'translateY(-150px)',
    transition: 'transform 1s ease'
};
const makeMoveUp = (Target) => {
    return class extends Component {
        constructor(props) {
            super(props);
            this.state = {moveTop: false};
        }

        onClick = () => {
            this.setState({moveTop: !this.state.moveTop});
        };

        render() {
            return (
                <Target isOpen={true}
                        onClick={this.onClick}
                        additionalStyles={{text: {}, frame: this.state.moveTop ? animationStyle : {}}}/>
            );
        }
    }
};
export default makeMoveUp;
view rawmove-up-animation.js hosted with ❤ by GitHub

Aceasta este ca makeExpanding Funcția HOC, cu excepția unei traduceri (deplasare în sus). De asemenea, stilul de animație se aplică doar cadrului exterior (div).

Actualizați App.js,


import React, {Component} from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';

// (Make material-ui happy)
// Needed for onTouchTap
// http://stackoverflow.com/a/34015469/988941
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();

import SearchBox from './SearchBox'
import makeMoveUp from './move-up-animation';
const MoveUpSearchBox = makeMoveUp(SearchBox);
class App extends Component {
    render() {
        //https://css-tricks.com/quick-css-trick-how-to-center-an-object-exactly-in-the-center/
        const style = {
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
        };

        return (
            <MuiThemeProvider>
                <div style={style}>
                    <MoveUpSearchBox/>
                </div>
            </MuiThemeProvider>
        );
    }
}
export default App;
view rawApp.js-2 hosted with ❤ by GitHub

și ar trebui să vezi

1611452710 484 Cum sa construiesti microinteractiuni animate in React
Tranziție CSS. transforma: traduceY

Poate că doriți un efect de salt. Ai putea folosi reacție-mișcare. Este o populară bibliotecă React care folosește dinamica primăverii pentru a controla animațiile. (O bună introducere, de Nash Vail, este aici.)

npm install --save react-motion

Crea spring-up-animation.js


import React, {Component} from 'react';
import {Motion, spring, presets} from 'react-motion'
const makeSpringUp = (Target) => {
    return class extends Component {
        constructor(props) {
            super(props);
            this.state = {moveTop: false};
        }

        onClick = () => {
            this.setState({moveTop: !this.state.moveTop});
        };

        render() {
            const style = {
                translateY: this.state.moveTop ? spring(-150, presets.wobbly) : spring(0)
            };
            return (
                <Motion style={style}>
                    {({translateY}) => (
                        <Target isOpen={true}
                                onClick={this.onClick}
                                additionalStyles={{
                                    text: {},
                                    frame: {
                                        transform: `translateY(${translateY}px)`
                                    }
                                }}/>
                    )}
                </Motion>
            );
        }
    }
};
export default makeSpringUp;
view rawspring-up-animation.js hosted with ❤ by GitHub

Deoarece acesta nu este un tutorial de reacție-mișcare, voi rezuma pe scurt cum funcționează acest lucru. React-motion înfășoară componenta animată, Target, cu componenta proprie, Motion. (Există și alte componente de reacție, cum ar fi TransitionMotion și Staggered Motion.)

React-motion interpolează, folosind dinamica arcului, o serie de valori intermediare. Oferă valorile componentei animate ca stil. Acest stil determină tranziția vizuală în animație.

Imaginea de mai jos arată rezultatul (cu un arc oscilant pentru a evidenția efectul).

1611452710 79 Cum sa construiesti microinteractiuni animate in React
dinamica arcului de reacție-mișcare

Puteți utiliza reacția-mișcare pentru o serie de efecte. De exemplu, puteți schimba caseta de text pentru a extinde ca un arc.

(spring-up-animation.js și move-up-animation.js au aceleași onClick logica stării, așa că am refactorizat părțile comune. Detaliile sunt aici.)

Vreau să ofer feedback utilizatorului cu privire la interogări eronate. Ați putea folosi mesaje de eroare, dar aș vrea să fac ceva mai capricios: scuturați caseta de căutare.

Aș putea folosi reacția-mișcare, dar aș vrea să analizez o altă tehnică: animația cadrului cheie.

React-animații este o bibliotecă React pentru animații cu cadre cheie. Se injectează cadre cheie CSS într-o foaie de stil DOM. (Celelalte exemple au folosit doar stiluri în linie.)

npm install --save react-animations

Am nevoie și de o bibliotecă Radiu sau Afrodita, pentru a gestiona injecția de foi CSS. Am ales Afrodita, așa cum am folosit-o înainte.

npm install --save aphrodite

Creați un alt HOC, shake-animation.js

import React, {Component} from 'react';
import {headShake} from 'react-animations';
import {StyleSheet, css} from 'aphrodite';
const styles = StyleSheet.create({
    headShake: {
        animationName: headShake,
        animationDuration: '1s'
    }
});
const makeValidationErrorAnimation = (Target) => {
    return class extends Component {
        constructor(props) {
            super(props);
            this.state = {shouldShake: false};
        }

        onClick = () => {
            this.setState({shouldShake: true}, () => {
                const self = this;
                setTimeout(() => self.setState({shouldShake: false}), 1000);
            });
        };

        render() {
            return (
                <Target isOpen={true}
                        onClick={this.onClick}
                        additionalStyles={{text: {}, frame: {}}}
                        frameClass={this.state.shouldShake ? css(styles.headShake) : ''}/>
            );
        }
    }
};
export default makeValidationErrorAnimation;

Există câteva secțiuni cheie. Linia 4 folosește Afrodita pentru a crea foaia de stil pentru efectul de reacție-animații, head-shake. Linia 29 setează clasa CSS pentru animație Target. (Acest lucru necesită o modificare pentru SearchBox pentru a utiliza clasa CSS. Uită-te la utilizarea frameClass în sursă de SearchBox.js.) The onClick handler pe linia 17 este mai complicat.

Repornirea unei animații

Aș vrea să fac „scuturarea capului” fiecare eroare de validare (sau orice declanșator utilizat). Dar, din moment ce animația este o clasă CSS, nu pot seta din nou aceeași clasă; nu ar avea niciun efect. Acest truc CSS post subliniază câteva opțiuni. Cel mai simplu este un timeout care elimină clasa de animație CSS. Când îl adăugați din nou (pentru un eveniment nou), veți vedea „scuturarea capului”.

1611452710 334 Cum sa construiesti microinteractiuni animate in React
animații de reacție (utilizează cadre cheie, animație CSS)

Împreună: compunerea unei componente complexe

Am creat mai multe HOC-uri pentru diferite animații. Dar puteți, de asemenea, să lanțați HOC-urile pentru a crea o componentă compusă. Va deschide caseta de text atunci când faceți clic și va scutura pe intrarea eronată.

Mai întâi, va trebui să faceți câteva modificări laSearchBox

import React from 'react';
import {TextField, IconButton} from 'material-ui'
import SearchIcon from 'material-ui/svg-icons/action/search';
const baseStyles = {
    open: {
        width: 300,
    },
    closed: {
        width: 0,
    },
    smallIcon: {
        width: 30,
        height: 30
    },
    icon: {
        width: 40,
        height: 40,
        padding: 5,
        top: 10
    },
    frame: {
        border: 'solid 1px black',
        borderRadius: 5
    }
};
const SearchBox = ({isOpen, query, onClick, onSubmit, onQueryUpdate, additionalStyles, frameClass}) => {
    const handleKeyDown = (event) => {
        const ENTER_KEY = 13;
        if (event.keyCode === ENTER_KEY) {
            event.preventDefault();
            onSubmit();
        }
    };
    let textStyle = isOpen ? baseStyles.open : baseStyles.closed;
    textStyle = Object.assign(textStyle, additionalStyles ? additionalStyles.text : {});
    const divStyle = Object.assign({}, textStyle, baseStyles.frame, additionalStyles ? additionalStyles.frame : {});
    divStyle.width += baseStyles.icon.width + 5;
    return (
        <div style={divStyle} className={frameClass ? frameClass : ''}>
            <IconButton iconStyle={baseStyles.smallIcon} style={baseStyles.icon} onClick={() => onClick()}>
                <SearchIcon />
            </IconButton>
            <TextField name="search"
                       style={textStyle}
                       value={query}
                       onKeyDown={handleKeyDown}
                       onChange={(event, value) => onQueryUpdate(value)}/>
        </div>
    );
};
export  default SearchBox;

SearchBox este acum un componentă controlată (termen elegant pentru utilizarea React pentru a gestiona valoarea de intrare a casetei de text). De asemenea, oferă un apel invers, onSubmit, pentru trimiterea interogării de căutare (atunci când un utilizator apasă pe introduce cheie).

De asemenea, trebuie să vă schimbați shake-animation.js. Dacă faceți clic pe pictograma de căutare nu ar trebui să provocați tremurături. În schimb, vreau o altă componentă pentru a determina când să „scutur”. Aceasta separă logica de validare de codul care controlează animația.

startShake este un steag pentru a reseta animația. Dar acesta este un detaliu de implementare. Ar trebui să fie încapsulat, ca stare internă, în makeShakeAnimation HOC.

import React, {Component} from 'react';
import {headShake} from 'react-animations';
import {StyleSheet, css} from 'aphrodite';
const styles = StyleSheet.create({
    headShake: {
        animationName: headShake,
        animationDuration: '1s'
    }
});
const makeShakeAnimation = (Target) => {
    return class extends Component {
        constructor(props) {
            super(props);
            this.state = {startShake: props.shouldShake};
        }

        componentWillReceiveProps(nextProps) {
            this.setState({startShake: nextProps.shouldShake}, () => {
                const self = this;
                setTimeout(() => self.setState({startShake: false}), 1000);
            });
            //https://css-tricks.com/restart-css-animation/ for discussion on restart
        }

        render() {
            return (
                <Target {...this.props}
                        frameClass={this.state.startShake ? css(styles.headShake) : ''}/>
            );
        }
    }
};
export default makeShakeAnimation;

startShake este dependent de shouldShake. pot folosi componentWillReceiveProps să răspundă la schimbările de prop. (Este părintele, componenta de validare, oferă aceste elemente de recuzită.) Așa că am mutat precedentul onClick logică pentru componentWillReceiveProps.

Schimbarea în linia 27, {...this.props}, trece toate elementele de recuzită componentei înfășurate, Target. (Trebuie să schimb în mod similar render metoda în expanding-animation.js. Detaliile sunt aici.)

Acum pot adăuga o componentă care va controla când să se agite.

Crea search-box-controller.js

import React, {Component} from 'react';

import makeExpanding from './expanding-animation';
import makeShakingAnimation from './shake-animation';

const makeAnimatedValidationSearchBox = (Target) => {
    const WrappedComponent = makeShakingAnimation(makeExpanding(Target));

    return class extends Component {
        constructor(props) {
            super(props);
            this.state = {query: '', hasError: false};
        }

        onQueryUpdate = (value) => {
            this.setState({query: value, hasError:false});
        };

        onSubmit = () => {
            this.setState({hasError: true});
        };

        render() {
            return (
                <WrappedComponent
                    onQueryUpdate={this.onQueryUpdate}
                    query={this.state.query}
                    onSubmit={this.onSubmit}
                    shouldShake={this.state.hasError}
                />
            );
        }
    }
};

export default makeAnimatedValidationSearchBox;

Acesta este un alt HOC. Nu are elemente vizuale, dar controlează comportamentul logic al componentei împachetate. (Dan Abramov are un bine post explicând o astfel de separare.) În acest caz, toate interogările sunt eronate, dar într-o aplicație reală, aș valida interogările și m-aș conecta la API-uri.

În cele din urmă, vreau să subliniez acest lucru makeAnimatedValidationSearchBox este un HOC care lanțează alte două HOC-uri.

const WrappedComponent =makeShakingAnimation(makeExpanding(Target));

O altă mică actualizare aApp.js

import React, {Component} from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';

// (Make material-ui happy)
// Needed for onTouchTap
// http://stackoverflow.com/a/34015469/988941
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();
import SearchBox from './SearchBox'

import makeAnimatedValidationSearchBox from './search-box-controller';
const AnimatedSearchBox = makeAnimatedValidationSearchBox(SearchBox);

class App extends Component {
    render() {
        //https://css-tricks.com/quick-css-trick-how-to-center-an-object-exactly-in-the-center/
        const style = {
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
        };
        return (
            <MuiThemeProvider>
                <div style={style}>
                    <AnimatedSearchBox/>
                </div>
            </MuiThemeProvider>
        );
    }
}
export default App;

(Linia 12 folosește noul HOC)

și executați run npm start

1611452711 840 Cum sa construiesti microinteractiuni animate in React
o componentă compusă, realizată din înlănțuirea a trei HOC

Am creat o componentă compusă care folosește microinteracțiuni multiple. Sunt reutilizabile și discrete.

Încheierea

Am probat fiecare dintre abordări: tranziții CSS, reacție-mișcare și reacție-animații. Aș dori să puteți alege o abordare, dar este greu să contorsionați o singură abordare pentru toate cazurile de utilizare. Din fericire, puteți combina bibliotecile și tehnicile. Și puteți încapsula detaliile în HOC-uri reutilizabile.

Poate doriți să verificați astfel de biblioteci recompune, care facilitează crearea HOC.

Repo GitHub pentru acest proiect este aici.

Vă rog ♡ această postare și urmați-mă pentru poveștile viitoare. Mulțumesc pentru lectură.