PS5.js de demonstração interativa
Aqui está uma demonstração da interface do usuário PS5 criada com animações JavaScript e CSS que iremos escrever neste tutorial. Um exemplo interativo pode ser tocado no artigo original .
Coloque um asterisco ou projeto forknite ps5.js 35,9 KB no GitHub.
Escrevi um tweet sobre a demonstração do PS3 quando estava construindo a versão básica da interface do usuário do console PS 3 em JavaScript . Ainda não tenho o código, mas pretendo publicá-lo. Além disso, este tutorial é baseado no conhecimento adquirido durante a criação do primeiro emprego.
Treinamento
Para não complicar nossa vida, não usaremos nenhum framework.
Mas mesmo se você usar estruturas ou bibliotecas, ainda precisa desenvolver seu próprio padrão para resolver o problema. Neste tutorial de IU, irei guiá-lo pelo próprio conceito por trás do desenvolvimento. Essa abordagem pode ser facilmente adaptada para React, Vue ou Angular.
Usei este arquivo HTML de modelo com estilos flex pré-construídos. Ele contém tudo que você precisa e a estrutura geral do aplicativo para começar. Este não é React ou Vue, mas é a configuração mínima necessária para criar um aplicativo. Eu uso esse espaço em branco sempre que preciso começar a trabalhar em um novo aplicativo ou site vanilla.
HTML e CSS
Nesta seção, explicarei alguns dos princípios básicos de criação de um arquivo HTML.
Estrutura simples de CSS DIY
Não sou um grande fã de frameworks CSS e prefiro começar do zero. No entanto, após milhares de horas de codificação, você começa a notar padrões recorrentes de qualquer maneira. Por que não criar algumas classes simples para cobrir os casos mais comuns? Isso nos impede de digitar os mesmos nomes de propriedade e valores centenas de vezes.
.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 }
Essas classes CSS falam por si.
Nossos primeiros estilos CSS
Agora que temos um CSS básico configurado, vamos adicionar alguns estilos para alterar a aparência dos contêineres de menu ocultos e exibidos. Lembre-se de que, como temos muitos menus e podemos alternar entre eles, precisamos designar de alguma forma quais menus estão “ligados” e quais estão “desligados”.
Por menus múltiplos, quero dizer que cada menu tem sua própria tela, definida por um elemento HTML separado. Ao alternar para o próximo menu, o contêiner anterior é oculto e o novo é exibido. As transições CSS também podem ser usadas para criar transições UX suaves, alterando a opacidade, a posição e a escala.
Todos os contêineres com uma classe
.menu
padrão estarão no estado "desligado" (ou seja, ocultos). Qualquer elemento com classes
.menu
e
.current
estará no estado “ligado” e será exibido na tela.
Outros elementos, como os botões selecionáveis no menu, usam a classe
.current
, mas em um contexto diferente da hierarquia CSS. Exploraremos seus estilos CSS nas próximas partes do tutorial.
#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
é novamente o contêiner pai padrão para todas as camadas de menu que criamos. Pode ser a tela do "navegador do jogo" ou a tela de "configurações". É invisível por padrão até que apliquemos a
classlist
classe à propriedade do elemento
.current
.
A
section.menu.current
indica o menu atualmente selecionado. Todos os outros menus devem estar invisíveis e a classe
.current
nunca deve ser aplicada a mais de um menu ao mesmo tempo!
Html
Nossa pequena estrutura CSS caseira simplifica muito o HTML. Aqui está o esqueleto principal:
<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>
Um elemento
ps5
é o contêiner principal do aplicativo.
A parte principal
flex
é
f v h
centralizar os elementos, portanto, veremos essa combinação com frequência.
Também nos encontraremos em
f r
vez de
flex-direction:row;
e em
f c
vez de
flex-direction:column;
.
Subseções são áreas separadas de um menu que requerem uma aula
menu
. Podemos alternar entre eles.
No código, eles serão enumerados pelo objeto congelado (veremos isso a seguir).
Substituindo o fundo
Uma das primeiras tarefas com que eu queria lidar era a função de mudança de plano de fundo. Se eu puder implementá-lo primeiro, então irei integrá-lo posteriormente em todas as funções futuras que precisam mudar o plano de fundo. Para isso, decidi criar dois
div
.
Quando o novo plano de fundo se torna ativo, eu simplesmente troco dois
div
, substituindo o valor da propriedade
style.background
pelo URL da nova imagem, e aplico uma classe ao novo plano de fundo
.fade-in
, removendo-o do anterior.
Comecei com o seguinte 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(https://semicolon.dev/static/playstation_5_teaser_v2.jpg); }
Em seguida, criei uma função estática auxiliar
.change
que se origina de uma classe
Background
que troca dois
div
e fade-los dentro ou fora (a função leva um argumento, o URL da próxima imagem):
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')
}
Agora, toda vez que precisar mostrar um novo fundo, simplesmente chamarei esta função com o URL da imagem a ser exibida:
Background.change('https://semicolon.dev/static/background-1.png')
O fade in será feito automaticamente porque
transform: 300ms
já foi aplicado a cada fundo e a classe
.fade-in
está fazendo o resto.
Como criar o menu de navegação principal
Agora que a estrutura básica está pronta, podemos começar a construir o restante da IU. Mas também precisamos escrever uma classe para gerenciar a IU. Vamos chamar essa classe
PS5Menu
. Vou explicar como usá-lo abaixo.
Tela do sistema
CSS simples foi usado para criar o botão Iniciar . Após pressionar o botão pelo usuário, vamos para o menu principal do PS5. Vamos colocar o botão Iniciar no primeiro menu da tela - no menu Sistema:
<section id = "system" class = "menu f v h">
<div id = "start" class = "f v h">Start</div>
</section>
Da mesma forma, o conteúdo de todos os outros menus estará localizado nos elementos do contêiner pai correspondente.
Veremos isso mais tarde. Agora precisamos descobrir como organizar várias telas de menu.
Neste ponto, precisamos aprender sobre o conceito de enfileiramento de vários menus. O PS5 possui várias camadas de diferentes interfaces de usuário de navegação. Por exemplo, quando você seleciona Configurações, um novo menu completamente diferente é aberto e o controle do teclado é transportado para esse novo menu.
Precisamos de um objeto para controlar todos esses menus que estão constantemente sendo abertos, fechados e substituídos por um menu novo ou anterior.
Você pode usar o método integrado
push
Objeto de matriz em JavaScript para adicionar um novo menu à fila. E quando precisamos retornar, podemos chamar o método
pop
array para retornar ao menu anterior.
Listamos o menu por atributo de
id
elemento:
const MENU = Object.freeze({
system: `system`,
main: `main`,
browser: `browser`,
settings: `settings`,
/* add more if needed*/
});
Usei
Object.freeze()
para que nenhuma das propriedades mudasse depois de configuradas. Alguns tipos de objetos são melhor congelados. Esses são os objetos que definitivamente não devem mudar ao longo da vida do aplicativo.
Aqui, cada valor é o nome da propriedade em formato de string. Dessa forma, podemos vincular os itens do menu por
MENU.system
ou
MENU.settings
. Não há nada além de estética sintática nessa abordagem, e também é uma maneira simples de evitar o armazenamento de todos os objetos de menu "em uma cesta".
Classe PS5Menu
Primeiro, criei uma classe
PS5Menu
. Seu construtor usa uma propriedade de
this.queue
tipo
Array
.
// menu queue object for layered PS5 navigation
class 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`)
}
}
Como faço para usar a classe PS5Menu?
Esta classe possui dois métodos, um setter e uma função estática . Eles farão quase a mesma coisa que os métodos de array e farão com o nosso array . Por exemplo, para criar uma instância do menu de classe e adicioná-la ou removê-la do menu da pilha, podemos chamar métodos e diretamente de uma instância da classe.
push(argument)
pop()
.push()
.pop
this.queue
push
pop
// instantiate the menu object from class
const menu = new PS5Menu()
// add menu to the stack
menu.push = `system`
// remove the last menu that was pushed onto the stack from it
menu.pop()
Funções configuradoras de classe como essa
set push()
não podem ser chamadas com
()
. Eles atribuem um valor usando um operador de atribuição
=
. A função de configurador de classe
set push()
será executada com este parâmetro.
Vamos combinar tudo o que já fizemos:
/* 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`
});
});
Aqui, criamos uma instância da classe
PS5Menu
e armazenamos sua instância de objeto em uma variável
menu
.
Em seguida, colocamos vários menus na fila com o primeiro menu com um id
#system
.
Em seguida, anexamos um evento ao botão Iniciar
click
. Quando clicamos neste botão, tornamos o menu principal (com
id
, igual a
main
) nosso menu atual. Nesse caso, o menu do sistema será oculto (o menu está atualmente na fila de menus) e o contêiner será exibido
#menu
.
Observe que, como nossa classe de contêiner de menu
.menu.current
tem a propriedade
transform: 400ms;
, então, com uma simples adição ou remoção de uma classe
.current
de um elemento, as propriedades recém-adicionadas ou removidas serão animadas em 0,4 milissegundos.
Agora você precisa pensar em como criar conteúdo para o menu principal.
Observe que esta etapa é realizada no evento DOM "Content Loaded" (
DOMContentLoaded
). Deve ser o ponto de entrada para qualquer aplicativo de UI. O segundo ponto de entrada é um evento
window.onload
, mas nesta demonstração não precisamos dele. Ele aguarda o término do download da mídia (imagens etc.), o que pode acontecer muito depois de os elementos DOM estarem disponíveis.
Tela de abertura
Inicialmente, a IU principal é uma série de vários elementos. A linha inteira aparece na borda direita da tela. Quando aparece pela primeira vez, ele se anima arrastando para a esquerda.
Incorporei esses elementos ao contêiner
#main
assim:
<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>
O primeiro menu PS5 é colocado dentro de um contêiner pai, com o seguinte estilo:
#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;
}
Por padrão, em seu estado oculto
#primary
, ele não é mostrado intencionalmente; ele é movido o suficiente para a direita (por 1200px).
Tivemos que passar por tentativa e erro e usar nossa intuição. Parece que 1200 px é um bom ajuste. Este contêiner também herda
opacity:0
da classe
.menu
.
Então, quando ele
#primary
aparece pela primeira vez, ele desliza e aumenta seu brilho ao mesmo tempo.
Aqui, novamente, o valor
transform:400ms;
(equivalente
0.4s
) é usado, porque a maioria das microanimações fica bem com
0.4s
. Valor
0.3s
também funciona bem, mas pode ser muito rápido ou
0.5s
muito lento.
Usando transições CSS para controlar animações de IU
Em vez de manipular manualmente os estilos CSS sempre que precisarmos alterar o estilo ou a posição do bloco da IU, podemos simplesmente atribuir e remover classes:
// 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")
Esta é uma estratégia importante que economizará muito tempo e manterá seu código limpo em qualquer projeto vanilla. Em vez de alterar a propriedade,
style.left
vamos apenas remover a classe
.hidden
do elemento
#primary
. Uma vez que foi
transform:400ms;
, a animação será reproduzida automaticamente.
Usaremos essa tática para alterar quase todos os estados dos elementos da IU.
Animação Slide-Out Secundária
Ao trabalhar com design UX, existem diferentes tipos de animações. Algumas animações são acionadas ao alternar para um novo menu. Eles geralmente começam após um curto período de tempo, logo após a mudança para uma nova tela.
Existem também animações de foco que são acionadas quando o mouse ou controlador seleciona um novo item adjacente no menu de navegação atual.
A atenção aos detalhes é importante, especialmente quando você está procurando criar um produto de qualidade.
Usando a função setTimeout para controlar os estados da animação
Uma pequena animação secundária é reproduzida conforme os itens são retirados . Para simular esse efeito duplo, uma função JavaScript foi usada
setTimeout
imediatamente depois que a árvore DOM foi totalmente carregada.
Como esta é a primeira tela de menu a aparecer logo após clicar no botão Iniciar , agora precisamos atualizar o evento do
click
botão Iniciar no evento DOMContentLoaded logo depois
menu.push = `main`
.
O código a seguir ficará na parte inferior de uma função de evento já existente
DOMContentLoaded
(consulte o exemplo de código-fonte mostrado acima):
/* 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)
});
});
O que resultou disso
Todo o código que escrevemos resultou nesta animação inicial:
Crie itens selecionáveis
Já criamos o CSS para os elementos selecionáveis (classe
.sel
).
Mas ainda parece rústico, não tão brilhante quanto a interface do PS5.
Na próxima seção, veremos as possibilidades de criar uma interface mais agradável. Vamos elevar a interface do usuário à aparência profissional do sistema de navegação PlayStation 5.
Animação padrão do elemento "selecionado" ou "atual"
Três tipos de animações para o item atualmente selecionado
Na interface do usuário do console PS5, os itens atualmente selecionados têm três efeitos visuais. Um contorno giratório - um "halo", um ponto de luz aleatório se movendo no fundo e, finalmente, uma "onda de luz" - um efeito que parece uma onda se movendo na direção do botão de direção pressionado no controlador.
Nesta seção, aprenderemos como criar o efeito clássico de contorno de botão do PS5 com um ponto de luz no fundo e uma onda de luz. Abaixo está uma análise de cada tipo de animação e as classes CSS de que precisamos para todos esses tipos:
Halo animado com gradiente
Este efeito adiciona uma borda animada que gira em torno do item selecionado.
Em CSS, isso pode ser simulado girando um gradiente cônico.
Aqui está um esboço geral de CSS para o elemento selecionável:
.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;
}
Tentei usar pseudo-elementos
::after
e
::before
, mas não consegui obter os resultados que desejo de maneiras simples, e seu suporte por navegadores está em questão; além disso, o JavaScript não tem uma maneira nativa de acessar pseudoelementos.
Em vez disso, decidi criar um novo elemento
.under
e diminuir sua posição Z em -1 usando
transform: translateZ(-1px)
; portanto, o movemos para longe da câmera, permitindo que seu pai apareça em cima dela.
Você também pode precisar adicionar uma
.sel
propriedade aos elementos pais identificados pelo elemento
transform-style: preserve-3d;
para habilitar a ordem z no espaço 3D do elemento.
Idealmente, gostaríamos de
.under
direcionar a camada ao elemento e criar um ponto de luz com o elemento de botão real dentro dele. Mas o truque tem
translateZ
uma prioridade mais alta e foi assim que comecei a construir a IU. Pode ser retrabalhado, mas nesta fase não é necessário.
HTML é muito simples. O importante aqui é que agora temos um novo elemento
.under
. Este é o elemento no qual o gradiente cônico giratório será renderizado para criar uma borda brilhante sutil.
.lightwave-container
nos ajudará a implementar o efeito de mover a luz com
overflow: hidden
.
.lightwave
- este é o elemento no qual o efeito será renderizado, é um div maior que vai além das bordas do botão e contém um gradiente radial de deslocamento.
<div id = "o0" data-id = "0" class = "sel b">
<div class = "under"></div>
<div class = "lightwave-container">
<div class = "lightwave"></div>
</div>
</div>
Desde o início de março de 2021, as animações CSS não são compatíveis com a rotação de fundo gradiente.
Para contornar esse problema, usei uma função JavaScript embutida
window.requestAnimationFrame
. Ele anima suavemente a propriedade do plano de fundo de acordo com a taxa de quadros do monitor, que geralmente é 60FPS.
// Continuously rotate currently selected item's gradient border
let 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)
Esta função é responsável por animar a borda giratória e o elemento de onda de luz maior.
O Paradigma do Listener de Eventos
Como não estamos usando React ou outras estruturas, precisamos lidar com os ouvintes de evento nós mesmos. Cada vez que alternamos o menu, precisamos desanexar todos os eventos do mouse de todos os itens dentro do contêiner pai do menu anterior e anexar ouvintes de eventos do mouse a todos os itens interativos dentro do contêiner pai do novo menu selecionado.
Cada tela é única. A maneira mais fácil é codificar os eventos para cada tela. Este não é um hack, mas simplesmente um código específico para cada sistema de navegação exclusivo. Para algumas coisas, simplesmente não existem soluções convenientes.
As próximas duas funções habilitarão e desabilitarão eventos de telas diferentes.
Veja o código-fonte completo do PS5.jspara entender como tudo funciona em geral.
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;
}
}
Isso garante que nunca escutemos mais eventos de mouse do que os que temos, para que o código UX seja executado de maneira ideal para cada tela de menu individual.
Navegar com o teclado
Os controles do teclado raramente são usados em aplicativos da web e sites. Então, criei uma biblioteca de teclado JS vanilla que reconhece teclas básicas e permite que você simplesmente conecte eventos de pressionamento de tecla.
Precisamos interceptar as seguintes chaves:
- Enter ou Espaço - Seleciona o item atualmente selecionado.
- Esquerda , Direita , Cima , Baixo - navegação pelo menu selecionado atualmente.
- Escape - Cancela o menu da fila atual e retorna ao menu anterior.
Você pode vincular todas as chaves básicas a variáveis da seguinte maneira:
// Map variables representing keys to ASCII codes
const [ 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;
Em seguida, crie um manipulador de eventos de teclado:
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++
}
}
E conecte-o ao objeto do documento:
document.body.addEventListener("keydown", keyboard_events_main_menu);
API de som
Ainda trabalhando nisso ...
Enquanto isso, você pode baixar aqui uma biblioteca API de som simples no vanilla JS.