Простая анимация кода дождя на основе React из трилогии Matrix

Это мой первый пост в блоге, и я хотел сделать его таким же особенным, каким обычно не бывает первый поцелуй.

Я был фанатом фильмов о Матрице с тех пор, как первый зеленый символ упал на тот старинный экран. Это одна из причин, по которой я хотел протестировать скорость рендеринга React и нажать, чтобы увидеть, где находится предел. Кроме того, всегда весело создавать что-то интересное. Как домик Лего или крепость из подушек.

Версия React, которую я использовал, - 16, и, поскольку я работал с коллекциями, конечно, Lodash (v4)

tl; dr - ›Если вы, как и я, ленивы, посмотрите полный код здесь: github.com/jasofalcon/matrix

Часть шрифта

Прежде всего, мы хотим, чтобы это выглядело профессионально. Кроме того, это может читать кто-то из Сиона. Я шучу, я знаю, что Сион не настоящий. В любом случае, я использовал Matrix Font NFI, посмотрите здесь - matrix-font-nfi. Мне кажется, это выглядит довольно Matrixy.
Еще я определил набор символов, которые будет использовать приложение, поскольку шрифт предоставляет символы только для некоторых букв. Вот так:

const chars = [
 'a',
 'b',
 'c',
 // ... many more symbols...
 '0',
 '~',
 '!',
 '#',
 '$'
];
export default chars;

Веселая часть

Хорошо, мистер Андерсон, давайте примет * красную таблетку * и нырнем в ужасную бездну слизи.
Структура приложения довольно проста и состоит всего из 3 компонентов. Символ, Код и Матрица!
Я хотел сделать его атомарным, поэтому Компонент символа представляет собой один-единственный символ и всю связанную с ним логику представления. Компонент кода - это не что иное, как массив символов, по сути, одна ниспадающая строка. Вы можете легко предположить, что этот компонент Matrix - не что иное, как массив компонентов кода, которые представляют собой списки символов. Итак, у нас есть список из списка символов, по математическому определению образующий …… .. матрицу символов.

Условное обозначение

Простой персонаж. Естественно, в качестве состояния он содержит символ.

constructor(props) {
    super(props);
    this.state = { char: this.getRandomChar() };
}

Вы заметили метод getRandomChar. Он делает не что иное, как получение случайного символа из списка символов, который мы определили ранее.

import chars from "../chars/chars";
// ...
getRandomChar() {
  return chars[Math.floor(Math.random() * chars.length)];
}

Во время просмотра фильма вы могли заметить, что некоторые символы меняются при падении на экран. В частности, последний символ (назовем его ведущим или основным) всегда меняется, как и некоторые другие символы тоже случайным образом. При этом давайте сделаем основной символ изменяемым и только «некоторые» другие.

componentWillMount() {
  if (this.props.primary || Math.random() > 0.95) {
    this.makeSymbolDynamic();
  }
}
makeSymbolDynamic() {
  setInterval(() => {
    this.setState({ char: this.getRandomChar() });
  }, 500);
}

Это может вызвать некоторые проблемы с производительностью, потому что мы регистрируем обработчик, который запускается каждые 500 мс. Поэтому мы делаем это только в том случае, если символ является основным и для 5% остальных. Math.rand возвращает значение от 0 до 1.

Еще одна важная деталь - непрозрачность. Каждая линия бледнеет к концу, поэтому есть еще одна опора, которую я отправляю компоненту Symbol, это непрозрачность. Вот как мы визуализируем символ:

render() {
    const { primary, opacity } = this.props;
    return (
        <div className={"symbol " + (primary ? "primary" : "")}
          style={{ opacity }} >
            {this.state.char}
        </div>
    );
}

Есть только один небольшой CSS-код, определяющий, что символ primary немного более блестящий, вот и все.

Код

А теперь вот где мы делаем тяжелую работу. Как уже упоминалось, компонент кода представляет собой набор символов. У нас есть несколько важных сведений об этой строке символов. Это положение (x, y), количество символов, скорость перехода (падения) и коэффициент масштабирования (некоторые ближе, а некоторые дальше).

constructor(props, state) {
  super(props);
this.state = {
    codeLength: 0,
    yPosition: 0,
    xPosition: 0,
    transition: "",
    transform: ""
  };
}

Перед установкой компонентов нам нужно все настроить. Это может показаться немного сложным, но это довольно просто, если вы посмотрите на каждую строку по отдельности. Кстати, большинство этих значений рассчитываются эмпирически.

const SYMBOL_HEIGHT = 30; // Empirically :)
const SYMBOL_WIDTH = 18;
componentWillMount() {
    // Some lines are zoomed-in, some zoomed-out
    const scaleRatio = _.random(0.8, 1.4); // Empirically chosen numbers
//Min code height is height of screen. No good reason but - why not
    const minCodeHeight = _.round(window.innerHeight / SYMBOL_HEIGHT);
    //This should calculate how much pixels does line take
    const codeLength = _.random(minCodeHeight, minCodeHeight * 2);
//Hacky solution to get the line above top=0 at start (hide it)
    const yPosition = (codeLength + 1) * SYMBOL_HEIGHT * scaleRatio;
// we don't want to have partially overlaping lines - make columns
    // it basically mean line can only fall in descrete positions
    const stepCount = _.round((window.innerWidth - 20) / SYMBOL_WIDTH);
    const xPosition = _.random(0, stepCount) * SYMBOL_WIDTH;
// we divide by scale ratio because if it is small it is probably far => thus slower :)
    const transition = ` top linear ${_.random(5, 10) / scaleRatio}s`; //different speed
    const transform = `scale(${scaleRatio})`;
this.setState({ codeLength, yPosition, xPosition, transition, transform });
  }

Следующее, что нам нужно сделать, это организовать падение. Мы не хотим, чтобы все упали одновременно, поэтому для одной строки мы установим случайное время начала. setTimeout гарантирует, что он запустится в какое-то псевдослучайное время

componentDidMount() {
  const startTime = _.random(300, 10000); // each starts in different time
setTimeout(() => {
    const newHeight = window.innerHeight + this.state.yPosition;
    this.setState({ yPosition: -newHeight }); //must be - b/c of start
  }, startTime);
}

Теперь все, что нам нужно сделать, это отрендерить компонент. Есть хороший трюк, как мы создаем разный вид непрозрачности для последних 5 символов.

render() {
  const code = _.times(this.state.codeLength).map((x, i) => (
    // Set opacity to small for last 5
    // last one will have least opacity, 5th from last will have almost full
    <Symbol key={i} opacity={i <= 5 ? i / 5 : 1} />
  ));
  
  const { yPosition, xPosition, transition, transform } = this.state;
  const styles = {
    left: xPosition,
    top: -yPosition,
    transition,
    transform
  };
// here we render list of symbols and one more - primary
  return (
    <div className="code" style={styles}>
    {code}
    <Symbol primary="true" />
    </div>
  );
}

Матрица

Не волнуйтесь, это самая легкая часть. Допустим, нам нужно всего 100 строк.

const CODE_LINES_COUNT = 100;
render() {
    const codes = _.times(CODE_LINES_COUNT).map((x, i) => <Code key={i} />);
    return <div className="Matrix">{codes}</div>;
}

И вот как получается - › демо

Забавный факт

При тестировании производительности рендеринга выяснилось, что при заданном количестве строк (~ 100) возможны лаги. Однако я должен признать, что Chrome - единственный браузер, который может отображать это без каких-либо заметных задержек. В то время как Safari, Edge и даже новый и сверхбыстрый Firefox немного борются. Так что, по крайней мере, вся оперативная память, которую пожирает Chrome, не зря.

Спасибо за прочтение, если вам понравилось, поделитесь аплодисментами :)

Если хотите оставить отзыв, пишите