Када прикључите контролер, гњечите дугмад, померате штапове, повлачите окидаче... и као програмер, не видите ништа од тога. Прегледач га преузима, наравно, али осим ако не евидентирате бројеве у конзоли, невидљив је. То је главобоља са Гамепад АПИ-јем. Постоји годинама, и заправо је прилично моћан. Можете читати дугмад, штапове, окидаче, радове. Али већина људи то не додирује. Зашто? Зато што нема повратних информација. Нема панела у алатима за програмере. Нема јасног начина да сазнате да ли контролор уопште ради оно што мислите. Осећај се као да летиш на слепо. То ме је довољно узнемирило да направим мали алат: Гамепад Цасцаде Дебуггер. Уместо да буљите у излаз конзоле, добијате живи, интерактивни поглед на контролер. Притисните нешто и оно реагује на екрану. А са ЦСС каскадним слојевима, стилови остају организовани, тако да је чистије за отклањање грешака. У овом посту ћу вам показати зашто су контролери за отклањање грешака толико мучни, како ЦСС помаже у чишћењу и како можете да направите визуелни дебугер за вишекратну употребу за сопствене пројекте.

Чак и ако сте у могућности да их све пријавите, брзо ћете завршити са нечитљивом нежељеном поштом на конзоли. на пример: [0,0,1,0,0,0.5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]

Можете ли рећи које дугме је притиснуто? Можда, али тек након што напрегнете очи и пропустите неколико уноса. Дакле, не, отклањање грешака не долази лако када је у питању читање улаза. Проблем 3: Недостатак структуре Чак и ако саставите брзи визуализатор, стилови могу брзо да постану неуредни. Подразумевано, активно и стање за отклањање грешака могу се преклапати, а без јасне структуре, ваш ЦСС постаје крхак и тешко га је проширити. ЦСС каскадни слојеви могу помоћи. Они групишу стилове у „слојеве“ који су поређани по приоритету, тако да престанете да се борите против специфичности и нагађате: „Зашто се мој стил отклањања грешака не приказује?“ Уместо тога, имате одвојене бриге:

База: Стандардни, почетни изглед контролера. Активно: Истакнуто за притиснуте дугмад и померене штапове. Отклањање грешака: Прекривачи за програмере (нпр. нумеричка очитавања, водичи и тако даље).

Ако бисмо дефинисали слојеве у ЦСС-у према овоме, имали бисмо: /* од најнижег до највишег приоритета */ @база слоја, активан, отклањање грешака;

@база слоја { /* ... */ }

@лаиер ацтиве { /* ... */ }

@лаиер дебуг { /* ... */ }

Пошто се сваки слој слаже предвидљиво, увек знате која правила побеђују. Та предвидљивост чини отклањање грешака не само лакшим, већ и управљивим. Покрили смо проблем (невидљив, неуредан унос) и приступ (визуелни дебагер изграђен са Цасцаде Лаиерс). Сада ћемо проћи кроз процес корак по корак да бисмо направили програм за отклањање грешака. Концепт дебагера Најлакши начин да скривени унос учините видљивим је да га једноставно нацртате на екрану. То је оно што овај програм за отклањање грешака ради. Дугмад, окидачи и џојстици добијају визуелни ефекат.

Притисните А: круг светли. Гурните штап: круг клизи около. Повуците окидач до пола: шипка се пуни до пола.

Сада не буљите у 0с и 1с, већ заправо гледате како контролер реагује уживо. Наравно, када почнете да гомилате стања као што су подразумевано, притиснуто, информације о отклањању грешака, можда чак и режим снимања, ЦСС почиње да постаје већи и сложенији. Ту су каскадни слојеви корисни. Ево скраћеног примера: @база слоја { .буттон { позадина: #222; граница радијуса: 50%; ширина: 40пк; висина: 40пк; } }

@лаиер ацтиве { .буттон.прессед { позадина: #0ф0; /* светло зелена */ } }

@лаиер дебуг { .буттон::афтер { садржај: аттр(податак-вредност); фонт-сизе: 12пк; боја: #ффф; } }

Редослед слојева је важан: база → активан → отклањање грешака.

база црта контролер. активно управља притиснутим стањима. дебуг баца на преклапања.

Разбијање на овај начин значи да се не борите са чудним ратовима специфичности. Сваки слој има своје место и увек знате шта побеђује. Буилдинг Ит Оут Хајде да прво нешто прикажемо на екрану. Не мора да изгледа добро – само треба да постоји да бисмо имали са чиме да радимо.

<х1>Гамепад Цасцаде Дебуггер

<див ид="цонтроллер"> <див ид="бтн-а" цласс="буттон">А <див ид="бтн-б" цласс="буттон">Б <див ид="бтн-к" цласс="буттон">Кс

<див> <див ид="паусе1" цласс="паусе"> <див ид="паусе2" цласс="паусе">

<буттон ид="тоггле">Укључи отклањање грешака

<див ид="статус">Дебугер је неактиван

<сцрипт срц="сцрипт.јс">

То су буквално само кутије. Још увек није узбудљиво, али нам даје ручице за касније коришћење ЦСС-а и ЈаваСцрипт-а. У реду, овде користим каскадне слојеве јер одржава ствари организованим када додате још стања. Ево грубог пролаза:

/* ==================================== ПОДЕШАВАЊЕ КАСКАДНИХ СЛОЈЕВА Редослед је важан: база → активно → отклањање грешака =====================================*/

/* Дефинишите редослед слојева унапред */ @база слоја, активан, отклањање грешака;

/* Слој 1: Основни стилови - подразумевани изглед */ @база слоја { .буттон { позадина: #333; граница радијуса: 50%; ширина: 70пк; висина: 70пк; дисплеј: флек; јустифи-цонтент: центар; алигн-итемс: центар; }

.паусе { ширина: 20пк; висина: 70пк; позадина: #333; дисплеј: инлине-блоцк; } }

/* Слој 2: Активна стања - управља притиснутим дугмадима */ @лаиер ацтиве { .буттон.ацтиве { позадина: #0ф0; /* Светло зелено када се притисне */ трансформ: скала(1.1); /* Благо увећава дугме */ }

.паусе.ацтиве { позадина: #0ф0; трансформ: сцалеИ(1.1); /* Протеже се вертикално када се притисне */ } }

/* Слој 3: Прекривачи за отклањање грешака - информације о програмеру */ @лаиер дебуг { .буттон::афтер { садржај: аттр(податак-вредност); /* Приказује нумеричку вредност */ фонт-сизе: 12пк; боја: #ффф; } }

Лепота овог приступа је у томе што сваки слој има јасну сврху. Основни слој никада не може заменити активно, а активни никада не може заменити отклањање грешака, без обзира на специфичност. Ово елиминише ратове специфичности ЦСС-а који обично муче алате за отклањање грешака. Сада изгледа као да неки кластери седе на тамној позадини. Искрено, није тако лоше.

Додавање ЈаваСцрипт-а ЈаваСцрипт време. Овде контролор заправо нешто ради. Изградићемо ово корак по корак. Корак 1: Подесите управљање стањем Прво, потребне су нам променљиве за праћење стања дебагера: // ===================================== // ДРЖАВНО УПРАВЉАЊЕ // =====================================

нека ради = фалсе; // Прати да ли је дебагер активан лет рафИд; // Чува ИД рекуестАниматионФраме за отказивање

Ове варијабле контролишу петљу анимације која непрекидно чита унос гејмпада. Корак 2: Узмите ДОМ референце Затим добијамо референце на све ХТМЛ елементе које ћемо ажурирати: // ===================================== // ДОМ ЕЛЕМЕНТ РЕФЕРЕНЦЕ // =====================================

цонст бтнА = доцумент.гетЕлементБиИд("бтн-а"); цонст бтнБ = доцумент.гетЕлементБиИд("бтн-б"); цонст бтнКс = доцумент.гетЕлементБиИд("бтн-к"); цонст паусе1 = доцумент.гетЕлементБиИд("паусе1"); цонст паусе2 = доцумент.гетЕлементБиИд("паусе2"); цонст статус = доцумент.гетЕлементБиИд("статус");

Чување ових референци унапред је ефикасније од сталног испитивања ДОМ-а. Корак 3: Додајте резервну тастатуру За тестирање без физичког контролера, мапираћемо тастере тастатуре на дугмад: // ===================================== // ТАСТАТУРА ФАЛЛБАЦК (за тестирање без контролера) // =====================================

цонст кеиМап = { "а": бтнА, "б": бтнБ, "к": бтнКс, "п": [пауза1, пауза2] // тастер 'п' контролише обе траке за паузу };

Ово нам омогућава да тестирамо кориснички интерфејс притиском на тастере на тастатури. Корак 4: Креирајте главну петљу ажурирања Ево где се магија дешава. Ова функција ради непрекидно и чита стање гејмпада: // ===================================== // ГЛАВНА ПЕТЉА ЗА АЖУРИРАЊЕ ГАМЕПАДА // =====================================

фунцтион упдатеГамепад() { // Набавите све повезане гамепаде цонст гамепадс = навигатор.гетГамепадс(); иф (!гамепадс) ретурн;

// Користи први повезани гамепад цонст гп = гамепадс[0];

иф (гп) { // Ажурирај стања дугмета пребацивањем „активне“ класе бтнА.цлассЛист.тоггле("активан", гп.буттонс[0].притиснут); бтнБ.цлассЛист.тоггле("активан", гп.буттонс[1].притиснут); бтнКс.цлассЛист.тоггле("активан", гп.буттонс[2].притиснут);

// Управљај дугметом за паузу (индекс дугмета 9 на већини контролера) цонст паусеПрессед = гп.буттонс[9].прессед; паусе1.цлассЛист.тоггле("ацтиве", паусеПрессед); паусе2.цлассЛист.тоггле("ацтиве", паусеПрессед);

// Направите листу тренутно притиснутих дугмади за приказ статуса пустити притиснут = []; гп.буттонс.форЕацх((бтн, и) => { ако (бтн.притиснут)прессед.пусх("Дугме " + и); });

// Ажурирај текст статуса ако се притисне било које дугме иф (притиснуто.дужина > 0) { статус.тектЦонтент = "Притиснуто: " + прессед.јоин(", "); } }

// Настави петљу ако је дебагер покренут ако (покреће) { рафИд = рекуестАниматионФраме(упдатеГамепад); } }

Метода цлассЛист.тоггле() додаје или уклања активну класу на основу тога да ли је дугме притиснуто, што покреће наше стилове ЦСС слојева. Корак 5: Управљајте догађајима на тастатури Ови слушаоци догађаја омогућавају да резервна тастатура функционише: // ===================================== // РУКОВАЧИ ДОГАЂАЈА НА ТАСТАТУРИ // =====================================

доцумент.аддЕвентЛистенер("кеидовн", (е) => { иф (кеиМап[е.кеи]) { // Руковање једним или више елемената иф (Арраи.исАрраи(кеиМап[е.кеи])) { кеиМап[е.кеи].форЕацх(ел => ел.цлассЛист.адд("ацтиве")); } остало { кеиМап[е.кеи].цлассЛист.адд("ацтиве"); } статус.тектЦонтент = "Тастер притиснут: " + е.кеи.тоУпперЦасе(); } });

доцумент.аддЕвентЛистенер("кеиуп", (е) => { иф (кеиМап[е.кеи]) { // Уклони активно стање када се отпусти кључ иф (Арраи.исАрраи(кеиМап[е.кеи])) { кеиМап[е.кеи].форЕацх(ел => ел.цлассЛист.ремове("ацтиве")); } остало { кеиМап[е.кеи].цлассЛист.ремове("ацтиве"); } статус.тектЦонтент = "Кључ ослобођен: " + е.кеи.тоУпперЦасе(); } });

Корак 6: Додајте Старт/Стоп контролу Коначно, потребан нам је начин да укључимо и искључимо програм за отклањање грешака: // ===================================== // УКЉУЧИ/ИСКЉУЧИ ДЕБУГГЕР // =====================================

доцумент.гетЕлементБиИд("тоггле").аддЕвентЛистенер("цлицк", () => { трчање = !трчање; // Обрни стање рада

ако (покреће) { статус.тектЦонтент = "Дебугер ради..."; упдатеГамепад(); // Покрени петљу ажурирања } остало { статус.тектЦонтент = "Дебугер је неактиван"; цанцелАниматионФраме(рафИд); // Заустави петљу } });

Дакле, да, притисните дугме и оно светли. Гурните штап и он се помера. то је то. Још једна ствар: сирове вредности. Понекад само желите да видите бројеве, а не светла.

У овој фази, требало би да видите:

Једноставан контролер на екрану, Дугмад која реагују док сте у интеракцији са њима, и Опционо очитавање за отклањање грешака које приказује индексе притиснутог дугмета.

Да ово буде мање апстрактно, ево кратке демонстрације контролера на екрану који реагује у реалном времену:

Сада, притиском на Започни снимање све се бележи док не притиснете Заустави снимање. 2. Извоз података у ЦСВ/ЈСОН Када будемо имали евиденцију, пожелећемо да је сачувамо.

<див цласс="цонтролс"> <буттон ид="екпорт-јсон" цласс="бтн">Извези ЈСОН <буттон ид="екпорт-цсв" цласс="бтн">Извези ЦСВ

Корак 1: Креирајте помоћник за преузимање Прво, потребна нам је помоћна функција која управља преузимањима датотека у прегледачу: // ===================================== // ПОМОЋНИК ЗА ПРЕУЗИМАЊЕ ДАТОТЕКА // =====================================

фунцтион довнлоадФиле(име датотеке, садржај, тип = "текст/обичан") { // Креирајте мрљу од садржаја цонст блоб = нови Блоб([садржај], { тип }); цонст урл = УРЛ.цреатеОбјецтУРЛ(блоб);

// Креирајте привремену везу за преузимање и кликните на њу цонст а = доцумент.цреатеЕлемент("а"); а.хреф = урл; а.довнлоад = име датотеке; а.цлицк();

// Очистите УРЛ објекта након преузимања сетТимеоут(() => УРЛ.ревокеОбјецтУРЛ(урл), 100); }

Ова функција функционише тако што креира Блоб (бинарни велики објекат) од ваших података, генерише привремени УРЛ за њега и програмски кликнете на везу за преузимање. Чишћење осигурава да не пропуштамо меморију. Корак 2: Руковање ЈСОН извозом ЈСОН је савршен за очување комплетне структуре података:

// ===================================== // ИЗВОЗ КАО ЈСОН // =====================================

доцумент.гетЕлементБиИд("екпорт-јсон").аддЕвентЛистенер("цлицк", () => { // Проверите да ли постоји нешто за извоз иф (!фрамес.ленгтх) { цонсоле.варн("Снимак није доступан за извоз."); повратак; }

// Креирајте корисни терет са метаподацима и оквирима цонст носивост = { цреатедАт: нови датум().тоИСОСТринг(), рамови };

// Преузми као форматиран ЈСОН преузми датотеку( "гамепад-лог.јсон", ЈСОН.стрингифи(корисно оптерећење, нулл, 2), "апплицатион/јсон" ); });

ЈСОН формат одржава све структурираним и лако рашчлањивим, што га чини идеалним за поновно учитавање у алате за програмере или дељење са саиграчима. Корак 3: Руковање ЦСВ извозом За ЦСВ извоз, морамо да сравнимо хијерархијске податке у редове и колоне:

//==================================== // ИЗВОЗ КАО ЦСВ // =====================================

доцумент.гетЕлементБиИд("екпорт-цсв").аддЕвентЛистенер("цлицк", () => { // Проверите да ли постоји нешто за извоз иф (!фрамес.ленгтх) { цонсоле.варн("Снимак није доступан за извоз."); повратак; }

// Направи ЦСВ ред заглавља (колоне за временску ознаку, сва дугмад, све осе) цонст хеадерБуттонс = фрамес[0].буттонс.мап((_, и) => бтн${и}); цонст хеадерАкес = фрамес[0].акес.мап((_, и) => акис${и}); цонст хеадер = ["т", ...хеадерБуттонс, ...хеадерАкес].јоин(",") + "\н";

// Направи ЦСВ редове података цонст ровс = фрамес.мап(ф => { цонст бтнВалс = ф.буттонс.мап(б => б.валуе); ретурн [ф.т, ...бтнВалс, ...ф.акес].јоин(","); }).јоин("\н");

// Преузми као ЦСВ довнлоадФиле("гамепад-лог.цсв", заглавље + редови, "текст/цсв"); });

ЦСВ је сјајан за анализу података јер се отвара директно у Екцел-у или Гоогле табелама, омогућавајући вам да креирате графиконе, филтрирате податке или визуелно уочите обрасце. Сада када су дугмад за извоз унутра, видећете две нове опције на табли: Извези ЈСОН и Извези ЦСВ. ЈСОН је згодан ако желите да баците необрађену евиденцију назад у своје програмерске алате или да провирујете по структури. ЦСВ се, с друге стране, отвара директно у Екцел или Гоогле табеле тако да можете да прикажете, филтрирате или упоредите уносе. Следећа слика приказује како панел изгледа са тим додатним контролама.

3. Систем снимка Понекад вам није потребан комплетан снимак, само брзи „снимак екрана“ стања уноса. Ту помаже дугме Сними снимак. <див цласс="цонтролс"> <буттон ид="снапсхот" цласс="бтн">Направи снимак

И ЈаваСцрипт:

// ===================================== // ТАКЕ СНАПСХОТ // =====================================

доцумент.гетЕлементБиИд("снапсхот").аддЕвентЛистенер("цлицк", () => { // Набавите све повезане гамепаде цонст падс = навигатор.гетГамепадс(); цонст ацтивеПадс = [];

// Прођите кроз петљу и снимите стање сваког повезаног гамепада фор (цонст гп оф падс) { ако (!гп) настави; // Прескочи празна места

ацтивеПадс.пусх({ ид: гп.ид, // Име/модел контролера временска ознака: перформанце.нов(), дугмад: гп.буттонс.мап(б => ({ притиснут: б.притиснут, вредност: б.вредност })), осе: [...гп.акес] }); }

// Проверите да ли су пронађени гамепади иф (!ацтивеПадс.ленгтх) { цонсоле.варн("Ниједан гамепад није повезан за снимак."); алерт("Није откривен контролер!"); повратак; }

// Пријавите се и обавестите корисника цонсоле.лог("Снапсхот:", ацтивеПадс); упозорење(Снимак направљен! Ухваћени ${ацтивеПадс.ленгтх} контролер(и).); });

Снимци замрзавају тачно стање вашег контролера у једном тренутку. 4. Гхост Инпут Реплаи Сада за оно забавно: понављање уноса духова. Ово узима дневник и репродукује га визуелно као да фантомски играч користи контролер.

<див цласс="цонтролс"> <буттон ид="реплаи" цласс="бтн">Понови последњи снимак

ЈаваСцрипт за поновну репродукцију: // ===================================== // ГХОСТ РЕПЛАИ // =====================================

доцумент.гетЕлементБиИд("реплаи").аддЕвентЛистенер("цлицк", () => { // Уверите се да имамо снимак за репродукцију иф (!фрамес.ленгтх) { алерт("Нема снимања за понављање!"); повратак; }

цонсоле.лог("Покретање репризе духа...");

// Праћење времена за синхронизовану репродукцију нека стартТиме = перформанце.нов(); нека фрамеИндек = 0;

// Реплаи анимација петље фунцтион степ() { цонст нов = перформанце.нов(); цонст елапсед = нов - стартТиме;

// Обради све оквире који су се до сада требали појавити вхиле (фрамеИндек < фрамес.ленгтх && фрамес[фрамеИндек].т <= елапсед) { цонст оквир = оквири[индекс оквира];

// Ажурирајте кориснички интерфејс са снимљеним стањима дугмади бтнА.цлассЛист.тоггле("ацтиве", фраме.буттонс[0].прессед); бтнБ.цлассЛист.тоггле("ацтиве", фраме.буттонс[1].прессед); бтнКс.цлассЛист.тоггле("ацтиве", фраме.буттонс[2].прессед);

// Ажурирај приказ статуса пустити притиснут = []; фраме.буттонс.форЕацх((бтн, и) => { иф (бтн.прессед) прессед.пусх("Буттон " + и); }); иф (притиснуто.дужина > 0) { статус.тектЦонтент = "Дух: " + прессед.јоин(", "); }

фрамеИндек++; }

// Настави петљу ако има више оквира иф (фрамеИндек < фрамес.ленгтх) { рекуестАниматионФраме(корак); } остало { цонсоле.лог("Реплаизавршено."); статус.тектЦонтент = "Поновна репродукција је завршена"; } }

// Покрени репризу корак(); });

Да бих отклањање грешака учинио мало практичнијим, додао сам понављање духова. Када снимите сесију, можете да притиснете реплаи и гледате како се кориснички интерфејс реагује, скоро као да фантомски плејер покреће пад. За ово се на панелу појављује ново дугме Реплаи Гхост.

Притисните Сними, поиграјте се мало са контролером, зауставите се, па поново репродукујте. Корисничко сучеље само одражава све што сте урадили, као дух који прати ваше уносе. Зашто се мучити са овим додацима?

Снимање/извоз олакшава тестерима да покажу шта се тачно догодило. Снимци се за тренутак замрзну, супер корисни када јурите чудне грешке. Гхост реплаи је одличан за туторијале, проверу приступачности или само упоређивање подешавања контроле једно поред другог.

У овом тренутку, то више није само уредна демонстрација, већ нешто што бисте могли да примените. Случајеви коришћења у стварном свету Сада имамо овај програм за отклањање грешака који може много тога. Приказује унос уживо, снима евиденције, извози их, па чак и репродукује ствари. Али право питање је: кога је заправо брига? Коме је ово корисно? Гаме Девелоперс Контролори су део посла, али њихово отклањање грешака? Обично бол. Замислите да тестирате комбинацију борбене игре, као што је ↓ → + ударац. Уместо да се молите, двапут сте га притиснули на исти начин, једном га снимите и поново репродукујете. Готово. Или замените ЈСОН дневнике са саиграчем да проверите да ли ваш код за више играча реагује исто на њиховој машини. То је огромно. Практичари приступачности Овај ми је близак срцу. Не играју се сви са „стандардним“ контролером. Адаптивни контролери понекад избацују чудне сигнале. Помоћу овог алата можете тачно видети шта се дешава. Наставници, истраживачи, ко год. Они могу да зграбе евиденције, упореде их или поново репродукују уносе један поред другог. Одједном, невидљиве ствари постају очигледне. Испитивање квалитета Тестери обично пишу белешке попут „Изгњечио сам дугмад овде и она се покварила“. Није од велике помоћи. Сада? Они могу да сниме тачне притиске, извезу дневник и пошаљу га. Без нагађања. Васпитачи Ако правите туторијале или ИоуТубе видео записе, реприза духова је златна. Можете дословно рећи: „Ево шта сам урадио са контролером“, док кориснички интерфејс показује да се то дешава. Чини објашњења јаснијим. Беионд Гамес И да, не ради се само о игрицама. Људи су користили контролере за роботе, уметничке пројекте и интерфејсе за приступачност. Сваки пут исти проблем: шта претраживач заправо види? Уз ово, не морате да погађате. Закључак Отклањање грешака на улазу контролера је одувек изгледало као да летите на слепо. За разлику од ДОМ-а или ЦСС-а, нема уграђеног инспектора за гамепаде; то су само необрађени бројеви у конзоли, који се лако губе у буци. Са неколико стотина линија ХТМЛ-а, ЦСС-а и ЈаваСцрипт-а, направили смо нешто другачије:

Визуелни дебагер који невидљиве улазе чини видљивим. Слојевити ЦСС систем који одржава кориснички интерфејс чистим и отклањањем грешака. Скуп побољшања (снимање, извоз, снимци, репродукција духова) која га подижу од демонстрације до алатке за програмере.

Овај пројекат показује колико далеко можете ићи мешањем моћи веб платформе са мало креативности у ЦСС каскадним слојевима. Алат који сам управо објаснио у целости је отвореног кода. Можете клонирати ГитХуб репо и испробати га сами. Али што је још важније, можете га учинити својим. Додајте своје слојеве. Изградите сопствену логику понављања. Интегришите га са својим прототипом игре. Или га чак користити на начине које нисам замишљао. За наставу, приступачност или анализу података. На крају крајева, не ради се само о отклањању грешака на гамепад-овима. Ради се о осветљавању скривених улаза и давању самопоуздања програмерима да раде са хардвером који веб још увек не прихвата у потпуности. Дакле, укључите свој контролер, отворите уређивач и почните да експериментишете. Можда ћете бити изненађени шта ваш претраживач и ваш ЦСС заиста могу да постигну.

You May Also Like

Enjoyed This Article?

Get weekly tips on growing your audience and monetizing your content — straight to your inbox.

No spam. Join 138,000+ creators. Unsubscribe anytime.

Create Your Free Bio Page

Join 138,000+ creators on Seemless.

Get Started Free