посмотрите на математику и код

1656512782 posmotrite na matematiku i kod

Флавио де Стефано

Это было 4 года назад, когда у меня возникла идея повторить Apple® Siri форма волны (введен в iPhone 4S) в браузере с использованием чистого Javascript.

В последний месяц я обновил эту библиотеку, сделав много рефакторинга с помощью функций ES6 и просмотрел процесс сборки с помощью RollupJS. Теперь я решил поделиться тем, что я узнал во время этого процесса и поделиться математикой, лежащей в основе этой библиотеки.

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

Кроме того, вы можете скачать все графики, нарисованные в этой статье в GCX (формат OSX Grapher): default.gcx и ios9.gcx.

Классический волновой стиль

U5DWfdAYQRgGntwYHyJQh2SPSbr2Eals8fD8
Классический стиль

Сначала эта библиотека имела только классический стиль волны, который вы все помните использовали в iOS 7 и iOS 8.

Воспроизвести эту простую форму волны не сложно, лишь немного математики и основных понятий Canvas API.

HGJeelo1DbenKSl72V423t-q89s3aBWrLet4
Форма волны Siri в iOS 7/8

Вы наверняка думаете, что форма волны является модификацией. Синус математическое уравнение, и вы правы… ну, почти правильно.

Прежде чем начать кодировку, мы должны найти наше линейное уравнение, которое потом будет просто применено. Мой любимый редактор сюжета Графист; вы можете найти его в любой инсталляции OSX в разделе Приложения > Утилиты > Graphr.app

Начнём с рисования известного:

-OIGYrieegxfDZ-rlhtkXmrrgBv6VgxZnb3f
AyYMYn3BxP7KdVZqlLdJ55gcqZHTHB5PFLVi
График для y = sin(x)

Хорошо! Теперь давайте добавим некоторые параметры (Amplitude [A]Координата времени[t] и пространственная частота [k]), которые будут полезны позже (подробнее здесь: https://en.wikipedia.org/wiki/Wave).

tDIRSzaKzb3bBDMJpQ2JfuHxwFPirZliPMV2

Теперь мы должны «ослабить» эту функцию на границе участка, чтобы |х| >; 2, тон Значение y стремится к 0. Проведем отдельно уравнениена g(x), имеющее эти характеристики.

EGqbusNiAWDyno0CSwWWpjmklbWccDUeypq1
L68Rd8wjrZV-9X9al6sme2Wi4kt7Z171E6bb

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

FWPk14LdAEnYMvGdv-X65IMf8pMPgJx6pO-5

Теперь, умножив наше f(x, …) и g(x, …), и установив точные параметры другим статическим значением, мы получим нечто вроде этого.

  • А = 0,9 установить амплитуду волны на max Y = A
  • k = 8 установите пространственную частоту, и мы получим больше пиков в диапазоне [-2, 2]
  • t = -π/2 настройте трансляцию фазы таким образом f(0, …) = 1
  • К=4 установите коэффициент для «уравнения ослабления» таким образом, чтобы конечное уравнение было y = 0, когда |х| ≥ 2
mI5c-n9vpwQWrtIK2pWz6R3gz6CCrJ0gRQ3s

Выглядит хорошо! ?

Теперь, если вы заметите на выходной волне, у нас есть другие волны, которые дадут меньшее значение для амплитуды. Давайте нарисуем их для A = {0,8, 0,6, 0,4, 0,2, -0,2, -0,4, -0,6, -0,8}

73RU94BxLIkS49r4TWBmA5IVuQwJAyTYPpF6

В конечной композиции полотна подволны будут нарисованы с уменьшением непрозрачности, стремящейся к 0.

Основные понятия кода

Что мы теперь делаем с этим уравнением?

Мы используем уравнение, чтобы получить Значение Y для вход X.

В основном, с помощью простого цикл for от -2 к 2, ( границы участка в этом случае)мы должны нарисовать пункт за пунктом уравнение на холсте с помощью beginPath и lineTo API.

const ctx = canvas.getContext('2d');
ctx.beginPath();ctx.strokeStyle="white";
for (let i = -2; i <= 2; i += 0.01) {   const x = _xpos(i);   const y = _ypos(i);   ctx.lineTo(x, y);}
ctx.stroke();

Вероятно, этот псевдокод прояснит эти идеи. Нам еще предстоит реализовать свое _xpos и _ypos функции.

Но… гей, что есть 0,01⁉️ Это значение представляет сколько пикселей вы двигаетесь вперед на каждой итерации, прежде чем достигнете правильного предела графика… но каково правильное значение?

Если вы используете очень небольшое значение (<0.01), вы получите безумно точное воспроизведение графика, но ваша производительность снизится, поскольку вы получите слишком много итераций.

Если вы используете действительно большое значение (>0.1) ваш график утратит точность, и вы заметите это мгновенно.

3c8OI5O8uiBqD8YUn7bp22xCmxHpCElp8pIh
График, нарисованный с точностью = 0,2

Вы можете увидеть, что окончательный код действительно похож на псевдокод: https://github.com/kopiro/siriwave/blob/master/src/curve.js#L25

Внедрить _xpos(i)

Вы можете утверждать, что если мы рисуем график, увеличивая xпотом _xpos может просто вернуть входящий аргумент.

Это почти правильно, но наш сюжет всегда тянется с к Б (B = Предел = 2).

Итак, рисовать на холсте через пиксельные координатымы должны перевести -B до 0, и Б к 1 (простой перенос [-B, B] к [0,1]); затем умножить [0,1] и ширина холста (w).

_xpos(i) = w * [ (i + B) / 2B ]

https://github.com/kopiro/siriwave/blob/master/src/curve.js#L19

Реализуйте _ypos

Воплощать _yposмы должны просто записать наше уравнение, полученное ранее (около).

const K = 4;const FREQ = 6;
function _attFn(x) {   return Math.pow(K / (K + Math.pow(x, K)), K);}
function _ypos(i) {   return Math.sin(FREQ * i - phase) *       _attFn(i) *       canvasHeight *      globalAmplitude *       (1 / attenuation);}

Давайте уточним некоторые параметры.

  • высота полотна – высота полотна, выраженная в PX
  • я это наше входное значение ( x)
  • фаза является важнейшим параметром, давайте обсудим его позже
  • globalAmplitude является статическим параметром, представляющим амплитуду общей волны (состоящей из подволн)
  • затухание является статическим параметром, который меняется для каждой линии и представляет амплитуду волны

https://github.com/kopiro/siriwave/blob/master/src/curve.js#L24

Фаза

Теперь давайте обсудим о фазовая переменная: это первая переменная переменная со временем, потому что имитирует движение волны.

Что это значит? Это означает, что для каждого кадр анимации, наш базовый контроллер должен прирост это значение. Но чтобы это значение не вызвало переполнение буфера, давайте помодулируем его с 2π (поскольку Math.sin dominio уже равно модулю 2π).

phase = (phase + (Math.PI / 2) * speed) % (2 * Math.PI);

Мы размножаемся скорость и Math.PI так что с скорость = 1 у нас максимальная скорость (почему? потому что sin(0) = 0, sin(π/2) = 1, sin(π) = 0, … ?)

Завершение

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

вернуться [   { attenuation: -2, lineWidth: 1.0, opacity: 0.1 },   { attenuation: -6, lineWidth: 1.0, opacity: 0.2 },   { attenuation: 4, lineWidth: 1.0, opacity: 0.4 },   { attenuation: 2, lineWidth: 1.0, opacity: 0.6},
   // basic line   { attenuation: 1, lineWidth: 1.5, opacity: 1.0},];

https://github.com/kopiro/siriwave/blob/master/src/siriwave.js#L190

Стиль iOS 9+

KAVRuTjxVxZvEQEIyG2xru3yzDpLZWvd8zdO
GIF SiriwaveJS iOS9+

Теперь все начинает усложняться. Стиль, введенный в iOS 9, действительно сложен, и для его имитации есть реверс-инжиниринг это совсем не легко! Я не полностью удовлетворен конечным результатом, но буду продолжать его совершенствовать, пока не достигну желаемого результата.

Как делали ранее, начнём получать линейные уравнения волн.

kxsuU2ovEPmN0mqiOwoWM3dHUYmG4wnRAQpc
Оригинальная форма сигнала Siri iOS 9+

Как вы можете заметить:

  • у нас три разные зеркальные уравнения с разными цветами (зеленый, синий, красный)
  • одна волна кажется a сумма уравнений синусов с разные параметры
  • все остальные цвета являются a композиция из этих трех основных цветов
  • есть a прямая линия на границе участка

Выбрав снова наши предыдущие уравнения, давайте определим более сложное уравнение предполагает перевод. Мы начинаем с того, что снова определяем наше уравнение ослабления:

PFv-Gz5oeue1rG-Wg06zngdoCsTpTPM83k6c

Теперь определите h(x, A, k, t) функция, то есть функция синуса умножить на функция ослабления, по абсолютному значению:

77pEjutms8rTTvzBaxAIX0dFAyqp6C5pChFp
gICWeQIDSMxE5jJMSZ2WQv6Kg5zmRQPT54tl

Теперь у нас есть мощный инструмент.

С h(x)теперь мы можем создать окончательную форму волны путем суммирования различных h(x) с разными параметрами, включающими разные амплитуды, частоту и трансляции. К примеру, давайте определим красная кривая путём ввода случайных значений.

hbal1DKzau5IyTSD4DaTdYc8pJpr3xZqd8Si
pLT6aOYpEHowx2xYoKy3Iqve6cqC9z4YADZ9

Если мы делаем то же самое из a зеленый и синий кривая, вот результат:

QAB6jCDUoq4uzllkLTbhIhKa0XCecjennMZL

Это не совсем идеально, но может сработать.

Чтобы получить зеркальную версию, просто умножьте все на -1.

Со стороны кодирования подход тот же, у нас есть только более сложное уравнение для _ypos.

const K = 4;const NO_OF_CURVES = 3;
// This parameters should be generated randomlyconst widths = [ 0.4, 0.6, 0.3 ];const offsets = [ 1, 4, -3 ];const amplitudes = [ 0.5, 0.7, 0.2 ];const phases = [ 0, 0, 0 ];
function _globalAttFn(x) {   return Math.pow(K / (K + Math.pow(x, 2)), K);}
function _ypos(i) {   let y = 0;   for (let ci = 0; ci < NO_OF_CURVES; ci++) {      const t = offsets[ci];      const k = 1 / widths[ci];      const x = (i * k) - t;            y += Math.abs(         amplitudes[ci] *          Math.sin(x - phases[ci]) *          _globalAttFn(x)      );   }
   y = y / NO_OF_CURVES;   return canvasHeightMax * globalAmplitude * y;}

Здесь нет ничего сложного. Единственное, что изменилось, это то, что мы ездим на велосипеде. NO_OF_CURVES раз за все псевдослучайные параметры и мы сумма все y значение.

Прежде чем умножить на canvasHeightMax и globalAmplitude которые дают нам абсолютную PX-координату холста, мы делим ее на NO_OF_CURVES так, чтобы y всегда ≤ 1.

https://github.com/kopiro/siriwave/blob/master/src/ios9curve.js#L103

Комбинированная операция

Здесь действительно важно одно globalCompositeOperation режим, который необходимо установить на Canvas. Если вы заметите, что в оригинальном контроллере, когда есть наложение 2+ цветов, они действительно смешиваются стандартным образом.

По умолчанию установлено значение источник-зано результат плохой, даже если установлена ​​непрозрачность.

fR8PeyeFbcJq-8Qycopohv6M1hfIK4Zudjal
составная операция: источник над

Вы можете увидеть все примеры варьировать globalCompositeOperation здесь: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation

С помощью настройки globalCompositeOperation к «светлее»вы заметите, что пересечение цветов ближе всего к оригиналу.

I5HGeo9b8U3bmt1XquQiRsdohkUZeRzIFE3x
Комбинированная работа: легче

Создайте с помощью RollupJS

Перед тем, как все переделать, я совсем не был доволен кодовой базой: старые классы, подобные прототипам, единственный файл Javascript для всего, ни одного uglify/minify и нет построения вообще.

Использование новой функции ES6, как родные классы, операторы распространения и лямбда-функциия смог очистить все, разделить файлы и снизить строки ненужного кода.

Кроме того, я использовал RollupJS для создания транспилированной и минимизированной сборки в разных форматах.

Поскольку это библиотека только для браузера, я решил создать два сборника: an UMD (определение универсального модуля) сборку, которую можно использовать непосредственно, импортируя сценарий или с помощью CDN, и еще одну в качестве файла Модуль ESM.

Модуль UMD построен с такой конфигурацией:

{   input: 'src/siriwave.js',   output: {      file: pkg.unpkg,      name: pkg.amdName,      format: 'umd'    },    plugins: [       resolve(),       commonjs(),       babel({ exclude: 'node_modules/**' }),    ]}

Дополнительный минимизированный модуль UMD построен с такой конфигурацией:

{   input: 'src/siriwave.js',   output: {      file: pkg.unpkg.replace('.js', '.min.js'),      name: pkg.amdName,      format: 'umd'    },    plugins: [       resolve(),       commonjs(),       babel({ exclude: 'node_modules/**' }),       uglify()]}

Пользуясь услугой UnPKG, вы можете найти окончательный сборник на этом URL, который обслуживает CDN: https://unpkg.com/siriwave/dist/siriwave.min.js

Это «способ Javascript старого стиля» – вы можете просто импортировать свой сценарий, а затем ссылаться на свой код с помощью SiriWave глобальный объект.

Чтобы обеспечить более элегантный и современный способ, я также создал ESM модуль с такой конфигурацией:

{    input: ‘src/siriwave.js’,   output: {       file: pkg.module,       format: ‘esm’   },    plugins: [       babel({ exclude: ‘node_modules/**’ })   ]}

Мы явно не хотим решить или commonjs Плагины RollupJS, потому что транслятор разработчика решает зависимость за нас.

Вы можете найти окончательную конфигурацию RollupJS здесь: https://github.com/kopiro/siriwave/blob/master/rollup.config.js

Просмотр и горячий код перезагрузки

Используя RollupJS, вы также можете воспользоваться преимуществами rollup-plugin-livereload и rollup-plugin-serve плагины, чтобы обеспечить лучший способ работы со скриптами.

По сути, вы просто добавляете эти плагины, находясь в режиме «разработчик»:

import livereload from 'rollup-plugin-livereload';import serve from 'rollup-plugin-serve';
if (process.env.NODE_ENV !== 'production') { additional_plugins.push(  serve({   open: true,   contentBase: '.'  }) ); additional_plugins.push(  livereload({   watch: 'dist'  }) );}

Завершаем, добавляя эти строки в package.json:

"module": "dist/siriwave.m.js","jsnext:main": "dist/siriwave.m.js","unpkg": "dist/siriwave.js","amdName": "SiriWave","scripts": {   "build": "NODE_ENV=production rollup -c",   "dev": "rollup -c -w"},

Давайте уточним некоторые параметры:

  • модуль / jsnext:main: путь к модулю dist ESM
  • unpkg: путь к модулю dist UMD
  • amdName: имя глобального объекта в модуле UMD

Большое спасибо RollupJS!

Надеюсь, что эта статья будет вам интересна, до встречи! ?

Добавить комментарий

Ваш адрес email не будет опубликован.