Русский
Русский
English
Статистика
Реклама

Перевод Как написать интерфейс пользователя (UI) PlayStation 5 на JavaScript

Интерактивное демо PS5.js


Вот демо интерфейса PS5, созданного при помощи анимаций на JavaScript и CSS, которые мы будем писать в этом туториале. Интерактивный пример можно потрогать в оригинале статьи.


Поставьте звёздочку или форкните проект ps5.js 35,9 КБ на GitHub.

Я написал твит о демо PS3, когда создавал на JavaScript базовую версию UI консоли PS3. Пока у меня нет кода, но я планирую его опубликовать. Более того, данный туториал построен на основании знаний, полученных при создании первой работы.

Подготовка


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

Но даже если вы используете фреймворки или библиотеки, то вам всё равно нужно разработать свой паттерн решения задачи. В этом туториале про UI я расскажу о самой концепции, лежащей в основе разработки. Этот подход можно легко адаптировать к React, Vue или Angular.

Я использовал эту заготовку HTML-файла с заранее созданными flex-стилями. Он содержит всё необходимое и общую структуру приложения, позволяющую приступить к работе. Это не React или Vue, но это та минимальная конфигурация, которая необходима для создания приложения. Я использую эту заготовку каждый раз, когда мне нужно начать работу над новым ванильным приложением или сайтом.

HTML и CSS


В этом разделе я объясню некоторые основы заготовки HTML-файла.

Простой самодельный CSS-фреймворк


Я не особый фанат CSS-фреймворков и предпочитаю начинать с чистого листа. Однако спустя тысячи часов кодинга вы в любом случае начинаете замечать часто повторяющиеся паттерны. Почему бы не создать несколько простых классов, охватывающих наиболее распространённые случаи? Благодаря этому нам не придётся сотни раз вводить одни и те же имена свойств и значения.

.rel { position: relative }.abs { position: absolute }.top { top: 0 }.left { left: 0 }.right { right: 0 }.bottom { bottom: 0 }/* flex */.f { display: flex; }.v { align-items: center }.vs { align-items: flex-start }.ve { align-items: flex-end }.h { justify-content: center }.hs { justify-content: flex-start }.he { justify-content: flex-end }.r { flex-direction: row }.rr { flex-direction: row-reverse }.c { flex-direction: column }.cr { flex-direction: column-reverse }.s { justify-content: space-around }.zero-padding { padding: 0 }.o { padding: 5px }.p { padding: 10px }.pp { padding: 20px }.ppp { padding: 30px }.pppp { padding: 50px }.ppppp { padding: 100px }.m { margin: 5px }.mm { margin: 10px }.mmm { margin: 20px }.mmmm { margin: 30px }

Эти классы CSS говорят сами за себя.

Наши первые CSS-стили


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

Под множественными меню я подразумеваю, что у каждого меню есть собственный экран, задаваемый отдельным HTML-элементом. При переключении на следующее меню предыдущий контейнер скрывается и отображается новый. Для создания плавных UX-переходов могут также использоваться CSS-переходы, изменяющие непрозрачность, позицию и масштаб.

Все контейнеры с классом .menu по умолчанию будут в состоянии отключен (off) (то есть скрыты). Любой элемент с классами .menu и .current будет находиться в состоянии включен (on) и отображаться на экране.

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

#ps5 {   width: 1065px;   height: 600px;   background: url('https://semicolon.dev/static/playstation_5_teaser_v2.jpg');   background-size: cover;}/* default menu container - can be any UI screen */#ps5 section.menu {    display: none;    opacity: 0;    // gives us automatic transitions between opacities    // which will create fade in/fade out effect.    // without writing any additional JavaScript    transition: 400ms;      }#ps5 section.menu.current {    display: flex;    opacity: 1;}

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

А section.menu.current обозначает текущее выбранное меню. Все остальные меню должны быть невидимыми, и класс .current никогда не должен одновременно применяться более чем к одному меню!

HTML


Наш самодельный крошечный CSS-фреймворк сильно упрощает HTML. Вот основной каркас:

<body>    <section id = "ps5" class = "rel">        <section id = "system" class = "menu f v h"></section>        <section id = "main" class = "menu f v h"></section>        <section id = "browser" class = "menu f v h"></section>        <section id = "settings" class = "menu f v h"></section>    </section></body>

Элемент ps5 это основной контейнер приложения.

Основная часть flex это f v h для центрирования элементов, поэтому это сочетание мы будем встречать часто.

Также мы встретим f r вместо flex-direction:row; и f c вместо flex-direction:column;.

Подразделы это отдельные области меню, требующие класса menu. Мы сможем переключаться между ними.

В коде они будут перечисляться замороженным объектом (мы увидим это ниже).

Замена фона


Одна из первых задач, с которой я хотел разобраться это функция смены фона. Если я смогу реализовать её сначала, то просто интегрирую её позже во все будущие функции, которым нужно будет менять фон. Для этого я решил создать два div.

Когда новый фон становится активным, я просто меняю местами два div, заменяя значение свойства style.background на URL нового изображения, и применяю к новому фону класс .fade-in, убирая его у предыдущего.

Я начал со следующего CSS:

#background-1, #background-2 {    position: absolute;    top: 0;    left: 0;    width: inherit;    height: inherit;    background: transparent;    background-position: center center;    background-size: cover;    pointer-events: none;    transition: 300ms;    z-index: 0;    opacity: 0;    transform: scale(0.9)}/* This class will be applied from Background.change() function */.fade-in { opacity: 1 !important; transform: scale(1.0) !important; z-index: 1 }/* set first visible background */#background-2 { background-image: url(http://personeltest.ru/aways/semicolon.dev/static/playstation_5_teaser_v2.jpg); }

Затем я создал вспомогательную статическую функцию .change, берущую начало от класса Background, который заменяет местами два div и выполняет их плавное понижение или повышение яркости (функция получает один аргумент URL следующего изображения):

class Background {constructor() {}}Background.change = url => {    console.log(`Changing background to ${url}`)    let currentBackground = $(`.currentBackground`);    let nextBackground = $(`.nextBackground`);    // set new background to url    nextBackground.style.backgroundImage = `url(${url})`    // fade in and out    currentBackground.classList.remove('fade-in')    nextBackground.classList.add('fade-in')    // swap background identity    currentBackground.classList.remove('currentBackground')    currentBackground.classList.add('nextBackground')    nextBackground.classList.remove('nextBackground')    nextBackground.classList.add('currentBackground')    }

Теперь каждый раз, когда мне понадобится показать новый фон, я просто буду вызывать эту функцию с URL изображения, которое нужно отобразить:

Background.change('https://semicolon.dev/static/background-1.png')

Постепенное увеличение яркости (fade in) будет реализовано автоматически, потому что transform: 300ms уже применено к каждому фону, а класс .fade-in занимается всем остальным.

Создаём основное меню навигации


Теперь, когда базовый каркас готов, мы можем начинать создание остальной части UI. Но нам нужно ещё написать класс для управления UI. Назовём этот класс PS5Menu. Как им пользоваться, объясню ниже.

Экран System


Для создания кнопки Start был использован простой CSS. После нажатия кнопки пользователем мы переходим в основное меню PS5. Поместим кнопку Start в первое меню на экране в меню System:

<section id = "system" class = "menu f v h">    <div id = "start" class = "f v h">Start</div></section>

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

До этого мы доберёмся позже. А сейчас нам надо решить, как организовать несколько экранов меню.

На данном этапе нам нужно узнать о концепции постановки в очередь нескольких меню. У PS5 есть несколько слоёв различных навигационных UI. Например, при выборе Settings открывается новое, совершенно другое меню, и управление с клавиатуры переносится в это новое меню.

Нам нужен объект для отслеживания всех этих меню, которые постоянно открываются, закрываются, а затем заменяются новым или предыдущим меню.

Можно воспользоваться встроенным методом push объекта Array в JavaScript, чтобы добавлять в очередь новое меню. А когда нам понадобится вернуться, мы сможем вызвать метод pop массива, чтобы вернуться к предыдущему меню.

Мы перечисляем меню по атрибуту id элемента:

const MENU = Object.freeze({    system: `system`,      main: `main`,   browser: `browser`,  settings: `settings`,/* add more if needed*/});

Я использовал Object.freeze(), чтобы ни одно из свойств после их задания не изменялось. Некоторые типы объектов лучше всего замораживать. Это те объекты, которые точно не должны меняться на протяжении срока жизни приложения.

Здесь каждое значение это имя свойства в строковом формате. Таким образом, мы сможем ссылаться на элементы меню по MENU.system или MENU.settings. В этом подходе нет ничего, кроме синтаксической эстетики, кроме того, это простой способ, позволяющий не хранить все объекты меню в одной корзине.

Класс PS5Menu


Для начала я создал класс PS5Menu. Его конструктор использует свойство this.queue типа Array.

// menu queue object for layered PS5 navigationclass PS5Menu {    constructor() {        this.queue = []    }    set push(elementId) {        // hide previous menu on the queue by removing "current" class        this.queue.length > 0 && this.queue[this.queue.length - 1].classList.remove(`current`)        // get menu container        const menu = $(`#${elementId}`)         // make the new menu appear by applying "current" class        !menu.classList.contains(`current`) && menu.classList.add(`current`)                // push this element onto the menu queue        this.queue.push( menu )         console.log(`Pushed #${elementId} onto the menu queue`)    }    pop() {        // remove current menu from queue        const element = this.queue.pop()        console.log(`Removed #${element.getAttribute('id')} from the menu queue`)    }}

Как использовать класс PS5Menu?


Этот класс имеет два метода, сеттер push(argument) и статическую функцию pop(). Они будут делать практически то же, что и методы массива .push() и .pop делают с нашим массивом this.queue.
Например, чтобы создать экземпляр класса меню и добавить или удалить из его стека меню, мы можем вызывать методы push и pop непосредственно из экземпляра класса.

// instantiate the menu object from classconst menu = new PS5Menu()// add menu to the stackmenu.push = `system`// remove the last menu that was pushed onto the stack from itmenu.pop()

Функции сеттеров классов наподобие set push() нельзя вызывать с (). Они присваивают значение при помощи оператора присваивания =. Функция сеттера класса set push() выполнится с этим параметром.

Давайте объединим всё, что мы уже сделали:

/* Your DOM just loaded */window.addEventListener('DOMContentLoaded', event => {          // Instantiate the queable menu    const menu = new PS5Menu()    // Push system menu onto the menu    menu.push = `system`    // Attach click event to Start button    menu.queue[0].addEventListener(`click`, event => {        console.log(`Start button pressed!`)        // begin the ps5 demo!        menu.push = `main`    });});

Здесь мы создали экземпляр класса PS5Menu и сохранили его экземпляр объекта в переменной menu.

Затем мы поместили в очередь нескольких меню первое меню с id #system.

Далее мы прикрепили к кнопке Start событие click. При нажатии на эту кнопку мы делаем основное меню (с id, равным main) нашим текущим меню. При этом меню системы скроется (меню в данный момент находится в очереди меню) и отобразится контейнер #menu.

Обратите внимание, что поскольку наш класс контейнера меню .menu.current имеет свойство transform: 400ms;, то при простом добавлении или удалении класса .current у элемента только что добавленные или удалённые свойства будут анимироваться в течение 0,4 миллисекунд.

Теперь нужно подумать над тем, как создавать контент для основного меню.

Обратите внимание, что этот шаг выполняется в событии DOM Content Loaded (DOMContentLoaded). Оно должно быть точкой входа для любого приложения с UI. Вторая точка входа это событие window.onload, но в данном демо оно нам не нужно. Оно ожидает завершения скачивания медиа (изображений и т. п.), что может произойти гораздо позже того, как стали доступными элементы DOM.

Экран заставки


Изначально основной UI представляет собой ряд из нескольких элементов. Весь ряд появляется с правого края экрана. При первом появлении он анимируется перемещением влево.

Я встроил эти элементы в контейнер #main следующим образом:

<section id = "main" class = "menu f v h">    <section id = "tab" class = "f">        <div class = "on">Games</div>        <div>Media</div>    </section>    <section id = "primary" class = "f">        <div class = "sel t"></div>        <div class = "sel b current"></div>        <div class = "sel a"></div>        <div class = "sel s"></div>        <div class = "sel d"></div>        <div class = "sel e"></div>        <div class = "sel"></div>        <div class = "sel"></div>        <div class = "sel"></div>        <div class = "sel"></div>        <div class = "sel"></div>    </section></section>

Первое меню PS5 помещается внутри родительского контейнера, стиль которого задаётся следующим образом:

#primary {    position: absolute;    top: 72px;    left: 1200px;    width: 1000px;    height: 64px;    opacity: 0;    /* animate at the rate of 0.4s */    transition: 400ms;}#primary.hidden {    left: 1200px;}

По умолчанию в своём скрытом состоянии (hidden) #primary намеренно не показывается, он и передвинут достаточно далеко вправо (на 1200px).

Нам пришлось двигаться путём проб и ошибок, а также воспользоваться интуицией. Похоже, неплохо подходит значение 1200px. Этот контейнер также наследует opacity:0 от класса .menu.

Поэтому когда #primary появляется в первый раз, он одновременно скользит и увеличивает свою яркость.

Здесь снова использовано значение transform:400ms; (эквивалентное 0.4s), потому что большинство микроанимаций выглядит красиво при 0.4s. Значение 0.3s тоже неплохо подходит, но может быть слишком быстрым, а 0.5s слишком медленным.

Используем CSS-переходы для управления анимациями UI


Вместо того, чтобы манипулировать CSS-стилями вручную каждый раз, когда нам понадобится изменить стиль или положение блока UI, мы можем просто назначать и удалять классы:

// get element:const element = $(`#primary`)// check if element already contains a CSS class:element.style.classList.contains("menu")// add a new class to element's class list:element.style.classList.add("menu")// remove a class from element's class list:element.style.classList.remove("menu")

Это важная стратегия, которая сэкономит кучу времени и сохранит чистоту кода в любом ванильном проекте. Вместо изменения свойства style.left мы просто удалим класс .hidden у элемента #primary. Так как он имеет transform:400ms;, автоматически воспроизведётся анимация.

Мы будем пользоваться этой тактикой для изменения почти каждого состояния элементов UI.

Вторичная анимация Slide-Out


При работе с дизайном UX существуют различные типы анимаций. Некоторые анимации срабатывают при переключении на новое меню. Обычно они начинаются спустя короткий промежуток времени, вскоре после переключения на новый экран.

Также существуют анимации при наведении курсора, срабатывающие, когда мышь или контроллер выбирает новый соседний элемент в текущем меню навигации.

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

Используем функцию setTimeout для управления состояниями анимаций


При выдвижении элементов воспроизводится вторичная небольшая анимация. Для имитации этого двойного эффекта использовалась функция JavaScript setTimeout сразу после полной загрузки дерева DOM.

Так как это первый экран меню, появляющийся вскоре после нажатия на кнопку Start, теперь нам нужно обновить событие click кнопки Start в событии DOMContentLoaded сразу после menu.push = `main`.

Следующий код будет находиться внизу уже существующей функции события DOMContentLoaded (см. показанный выше пример исходного кода):

/* Your DOM just loaded */window.addEventListener('DOMContentLoaded', event => {          /* Initial setup code goes here...see previous source code example */    // Attach click event to Start button    menu.queue[0].addEventListener(`click`, event => {        console.log(`Start button pressed!`)        // begin the ps5 demo!        menu.push = `main`        // new code: animate the main UI screen for the first time        // animate #primary UI block within #main container        primary.classList.remove(`hidden`)        primary.classList.add(`current`)        // animate items up        let T1 = setTimeout(nothing => {                      primary.classList.add('up');            def.classList.add('current');            // destroy this timer            clearInterval(T1)            T1 = null;        }, 500)    });    });

Что из этого вышло


Весь написанный нами код привёл к созданию такой начальной анимации:


Создаём выбираемые элементы


Мы уже создали CSS для выбираемых элементов (класс .sel).

Но он пока выглядит простовато, не так блестящ, как интерфейс PS5.

В следующем разделе мы рассмотрим возможности создания более красивого интерфейса. Мы поднимем уровень UI до профессионального внешнего вида системы навигации PlayStation 5.

Стандартная анимация выбранного или текущего элемента


Три типа анимаций текущего выбранного элемента


В UI консоли PS5 текущие выбранные элементы имеют три визуальных эффекта. Вращающийся контур-гало, случайное пятно света, движущееся на фоне и, наконец, световая волна эффект, выглядящий как волна, движущаяся в направлении нажатой на контроллере кнопки направления.

В этом разделе мы узнаем, как создать классический эффект сияющего контура кнопки PS5 с пятном света на фоне и эффектом световой волны. Ниже представлен анализ каждого типа анимации и CSS-классы, которые нам понадобятся для всех этих типов:

Анимированное гало с градиентом


Этот эффект добавляет анимированную границу, которая вращается вокруг выбранного элемента.

В CSS это можно имитировать вращением конического градиента.

Вот общая схема CSS выбираемого элемента:

.sel {    position: relative;    width: 64px;    height: 64px;    margin: 5px;    border: 2px solid #1f1f1f;    border-radius: 8px;    cursor: pointer;    transition: 400ms;    transform-style: preserve-3d;    z-index: 3;}.sel.current {    width: 100px;    height: 100px;    }.sel .under {    content:'';    position: absolute;    width: calc(100% + 8px);    height: calc(100% + 8px);    margin: -4px -4px;    background: #1f1f1f;    transform: translateZ(-2px);    border-radius: 8px;    z-index: 1;}.sel .lightwave-container {    position: relative;    width: 100%;    height: 100%;    transition: 400ms;    background: black;    transform: translateZ(-1px);    z-index: 2;    overflow: hidden;}.sel .lightwave {    position: absolute;    top: 0;    right: 0;    width: 500%;    height: 500%;        background: radial-gradient(circle at 10% 10%, rgba(72,72,72,1) 0%, rgba(0,0,0,1) 100%);    filter: blur(30px);    transform: translateZ(-1px);    z-index: 2;    overflow: hidden;}

Я пытался использовать псевдоэлементы ::after и ::before, но не смог простыми способами добиться нужных мне результатов, а их поддержка браузерами находится под вопросом; к тому же, в JavaScript нет нативных способов доступа к псевдоэлементам.


Вместо них я решил создать новый элемент .under и уменьшить его позицию по оси Z на -1 при помощи transform: translateZ(-1px); таким образом, мы отодвинули его от камеры, позволив его родительскому элементу отображаться поверх него.

Возможно, также понадобится добавить родительским элементам, идентифицируемым по .sel, свойство transform-style: preserve-3d;, чтобы включить z-order в 3D-пространстве элемента.

В идеале нам бы хотелось сделать слой .under родителем элемента и создать пятно света с самим элементом кнопки внутри него. Но у трюка с translateZ приоритет выше, к тому же, именно так я начал создавать UI. Его можно переработать, но на данном этапе это необязательно.

С HTML всё достаточно просто. Важно здесь то, что теперь у нас есть новый элемент .under. Это элемент, на котором будет рендериться вращающийся конический градиент для создания тонкой сияющей границы.

.lightwave-container поможет нам реализовать эффект перемещения света при помощи overflow: hidden. .lightwave это элемент, на котором будет рендериться сам эффект, он является более крупным div, выходящим за границы кнопки и содержащим смещённый радиальный градиент.

<div id = "o0" data-id = "0" class = "sel b">    <div class = "under"></div>    <div class = "lightwave-container">        <div class = "lightwave"></div>    </div></div>

На начало марта 2021 года CSS-анимация не поддерживает градиентного вращения фона.

Чтобы обойти это затруднение, я воспользовался встроенной функцией JavaScript window.requestAnimationFrame. Она плавно анимирует свойство фона в соответствии с частотой кадров монитора, которая обычно равна 60FPS

// Continuously rotate currently selected item's gradient borderlet rotate = () => {    let currentlySelectedItem = $(`.sel.current .under`)    let lightwave = $(`.sel.current .lightwave`)    if (currentlySelectedItem) {        let deg = parseInt(selectedGradientDegree);        let colors = `#aaaaaa, black, #aaaaaa, black, #aaaaaa`;        // dynamically construct the css style property        let val = `conic-gradient(from ${deg}deg at 50% 50%, ${colors})`;        // rotate the border        currentlySelectedItem.style.background = val        // rotate lightwave        lightwave.style.transform = `rotate(${selectedGradientDegree}deg)`;        // rotate the angle        selectedGradientDegree += 0.8    }    window.requestAnimationFrame(rotate)}window.requestAnimationFrame(rotate)

Эта функция отвечает за анимирование вращающейся границы и более крупного элемента световой волны.

Парадигма Event Listener


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

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

Две следующие функции будут подключать и отключать события от различных экранов.

См. полный исходный код PS5.js, чтобы понять, как всё устроено в целом.

function AttachEventsFor(parentElementId) {    switch (parentElementId) {        case "system":          break;        case "main":          break;        case "browser":          break;        case "settings":          break;    }}function RemoveEventsFrom(parentElementId) {    switch (parentElementId) {        case "system":          break;        case "main":          break;        case "browser":          break;        case "settings":          break;    }}

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

Навигация с помощью клавиатуры


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

Нам необходимо перехватывать следующие клавиши:

  • Enter или Space выбирают текущий выбранный элемент.
  • Left, Right, Up, Down навигация по текущему выбранному меню.
  • Escape отменяет текущее меню, находящееся в очереди, и выполняет возврат к предыдущему меню.

Привязать все основные клавиши к переменным можно следующим образом:

// Map variables representing keys to ASCII codesconst [ A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z ] = Array.from({ length: 26 }, (v, i) => 65 + i);const Delete = 46;const Shift = 16;const Ctrl = 17;const Alt = 18;const Left = 37;const Right = 39;const Up = 38;const Down = 40;const Enter = 13;const Return = 13;const Space = 32;const Escape = 27;

А затем создать обработчик событий клавиатуры:

function keyboard_events_main_menu(e) {    let key = e.which || e.keyCode;    if (key == Left) {        if (menu.x > 0) menu.x--    }    if (key == Right) {        if (menu.x < 3) menu.x++    }    if (key == Up) {        if (menu.y > 0) menu.y--    }    if (key == Down) {        if (menu.y < 3) menu.y++    }}

И подключить его к объекту документа:

document.body.addEventListener("keydown", keyboard_events_main_menu);

Звуковой API


Всё ещё работаю над ним

А пока вы можете скачать отсюда простую библиотеку звукового API на ванильном JS.
Источник: habr.com
К списку статей
Опубликовано: 16.03.2021 20:17:50
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании timeweb

Javascript

Программирование

Playstation

Timeweb

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru