Fazemos um plano interativo do terreno em 15 minutos


Na Torradeira, eles costumam perguntar como fazer um diagrama interativo de uma casa, um plano de sua estrutura interna, a capacidade de selecionar pisos ou apartamentos com informações sobre eles, exibir informações sobre os detalhes de um produto específico quando você passa o mouse sobre eles em uma fotografia, etc. Não se trata de um modelo tridimensional, mas de uma imagem com a capacidade de destacar determinados detalhes. Todas essas tarefas são semelhantes e resolvidas de maneira bastante simples, mas, mesmo assim, as perguntas continuam aparecendo, então hoje veremos como essas coisas são feitas usando o SVG, um editor gráfico e uma pitada de javascript.


A escolha do SVG se deve ao fato de ser a opção mais simples para desenvolvimento e depuração. Eu conheci pessoas que aconselharam que tudo isso fosse feito na tela, mas é muito mais difícil entender o que está acontecendo, e as coordenadas de todos os pontos nas curvas precisam ser calculadas com antecedência de alguma forma, mas aqui abri as ferramentas do desenvolvedor e imediatamente vi toda a estrutura, todos os objetos, com a qual há interação, e tudo o mais pode ser clicado com o mouse em uma interface amigável ao ser humano. O desempenho entre a tela 2D comum e o SVG dificilmente será diferente. O WebGL pode oferecer algum bônus nesse sentido, mas o cronograma de desenvolvimento aumentará significativamente, sem mencionar o suporte adicional, que nem sempre se encaixa no orçamento. Eu diria mesmo "nunca se encaixa".


Neste tutorial, faremos algo como um widget para um site fictício para alugar casas particulares em uma determinada área, mas é claro que os princípios para criar essas coisas em questão se aplicam a qualquer tópico.


Introdução


Mostrarei tudo com o Inkscape como exemplo, mas todas as mesmas ações podem ser executadas em qualquer outro editor usando funções semelhantes.


Para trabalhar, precisamos de duas caixas de diálogo principais:


  • Editor de XML (Ctrl + Shift + X ou um ícone com colchetes angulares) - para exibir a estrutura do documento na forma de marcação e editar elementos individuais.
  • Preenchimento e traçado (Ctrl + Shift + F ou o ícone com um pincel no quadro) - principalmente para preencher contornos.

Inicie-os imediatamente e continue com a criação do documento.


Se você os arrastou acidentalmente para uma janela separada, clique no quadro superior desta janela (onde não há nada) e arraste-os de volta para a janela principal. Isso não é totalmente intuitivo, mas conveniente.

Abra uma foto com vista para a área. Podemos optar por inserir a própria imagem como uma string base64 ou um link externo. Como é grande, selecione o link. Em seguida, mudaremos o caminho da imagem com nossas mãos ao introduzir tudo nas páginas do site. Um documento SVG é criado no qual a foto será incorporada por meio da tag da imagem.


Para imagens raster incorporadas ao SVG, incorporadas ao HTML, você pode usar o carregamento lento, bem como para imagens comuns nas páginas. Neste exemplo, não vamos nos debruçar sobre essas otimizações, mas não as esquecemos no trabalho prático.

No estágio atual, vemos diante de nós algo assim:



Agora crie uma nova camada (Ctrl + Shift + N ou Menu> Camada> Adicionar camada). No editor XML, vemos que o elemento g regular apareceu. Embora não tenhamos ido longe, podemos dar uma aula, que usaremos posteriormente nos scripts.


Não confie na identificação. Quanto mais complexa a interface, mais fácil é repeti-las e obter bugs estranhos. E em nossa tarefa, eles ainda não têm benefício. Portanto, classes ou atributos de dados são a nossa escolha.

Se você olhar atentamente para a estrutura do documento no editor XML, perceberá que há muito supérfluo. Qualquer editor de gráficos vetoriais mais ou menos complexo adicionará algo próprio aos documentos. Para remover tudo isso com as mãos é uma tarefa longa e ingrata, o editor adicionará algo constantemente novamente. Portanto, a limpeza do SVG do lixo é feita apenas no final do trabalho. E de preferência de forma automatizada, já que existem opções prontas, o mesmo svgo, por exemplo.


Encontre uma ferramenta chamada Desenhar curvas de Bezier e Linhas retas (Shift + F6). Com isso, desenharemos contornos fechados ao redor dos objetos. Em nossa tarefa, precisamos delinear todos os edifícios. Por exemplo, nos restringimos a seis, mas em condições reais vale a pena pré-alocar tempo para delinear com precisão todos os objetos necessários. Embora muitas vezes ocorram muitas entidades semelhantes, os mesmos andares em um prédio podem ser absolutamente idênticos. Nesses casos, você pode acelerar um pouco e copiar e colar as curvas.


Depois de circularmos os prédios necessários, retornamos ao editor XML, adicionamos classes ou, provavelmente, será ainda mais conveniente, atributos de dados com índices para eles (é possível com endereços, mas como temos uma área fictícia, há apenas índices) e mova tudo para a camada criada anteriormente, para que tudo fique "disposto nas prateleiras". E a imagem, a propósito, também será útil para se mudar para lá, de modo que tudo esteja em um só lugar, mas essas são insignificantes.


Agora, tendo escolhido um caminho - uma curva ao redor do edifício, você pode selecioná-los todos com Ctrl + A ou Menu> Editar> Selecionar tudo e editar ao mesmo tempo. Você precisa pintá-los todos na janela Preenchimento e traçado e, ao mesmo tempo, remover o traçado extra. Bem, ou adicione-o, se necessário, por motivos de design.



Faz sentido pintar todos os contornos com alguma cor, com um valor mínimo de opacidade, mesmo que isso não seja necessário em termos de design. O fato é que navegadores "inteligentes" acreditam que você não pode clicar em um caminho vazio, mas em um caminho inundado - você pode, mesmo que ninguém veja esse preenchimento.

Em nosso exemplo, deixaremos um pouco de destaque em branco para ver melhor com quais edifícios trabalhamos, salvar tudo e passar facilmente para o navegador e o editor de código mais familiar.


Exemplo básico


Vamos criar uma página html vazia, colar o SVG resultante diretamente nela e adicionar um pouco de CSS para que nada saia da tela. Não há nada para comentar.


.map { width: 90%; max-width: 1300px; margin: 2rem auto; border: 1rem solid #fff; border-radius: 1rem; box-shadow: 0 0 .5rem rgba(0, 0, 0, .3); } .map > svg { width: 100%; height: auto; border-radius: .5rem; } 

Lembramos que adicionamos classes aos edifícios e as usamos para que o CSS seja mais ou menos estruturado.


 .building { transition: opacity .3s ease-in-out; } .building:hover { cursor: pointer; opacity: .8 !important; } .building.-available { fill: #0f0 !important; } .building.-reserved { fill: #f00 !important; } .building.-service { fill: #fff !important; } 

Como definimos os estilos embutidos no Inkscape, precisamos interrompê-los no CSS. Seria mais conveniente fazer tudo em CSS? Sim e não Depende da situação. Às vezes não há escolha. Por exemplo, se um designer desenhou um monte de tudo colorido e embrulhou tudo em CSS e o inflou à impossibilidade de, de alguma forma, não ser confirmado. Neste exemplo, uso a opção “inconveniente” para mostrar que não é particularmente assustador no contexto do problema que está sendo resolvido.



Suponha que recebamos novos dados sobre as casas e adicione classes diferentes a elas, dependendo do status atual:


 const data = { id_0: { status: 'service' }, id_1: { status: 'available' }, id_2: { status: 'reserved' }, id_3: { status: 'available' }, id_4: { status: 'available' }, id_5: { status: 'reserved' }, messages: { 'available': '  ', 'reserved': '', 'service': '  1-2 ' } }; const map = document.getElementById('my-map'); const buildings = map.querySelectorAll('.building'); for (building of buildings) { const id = building.getAttribute('data-building-id'); const status = data[`id_${id}`].status; building.classList.add(`-${status}`); } 

Temos algo parecido com isto:



Algo semelhante ao que precisamos já está visível. Nesta fase, destacamos objetos no terreno que respondem à passagem do mouse. E não é difícil adicionar uma resposta a um clique do mouse no addEventListener padrão.


Linha de líder


Muitas vezes, há a tarefa de criar linhas que conectam os objetos no mapa e alguns elementos da página com informações adicionais, além de fazer dicas mínimas ao passar o mouse sobre esses mesmos objetos. Para resolver esses problemas, a mini-biblioteca da linha de líderes é muito adequada, o que cria setas vetoriais para todos os gostos e cores.


Vamos adicionar preços para dicas de ferramentas aos dados e desenhar essas linhas.


 const data = { id_0: { price: '3000', status: 'service' }, id_1: { price: '3000', status: 'available' }, id_2: { price: '2000', status: 'reserved' }, id_3: { price: '5000', status: 'available' }, id_4: { price: '2500', status: 'available' }, id_5: { price: '2500', status: 'reserved' }, messages: { 'available': '  ', 'reserved': '', 'service': '   (1-2 )' } }; const map = document.getElementById('my-map'); const buildings = map.querySelectorAll('.building'); const info = map.querySelector('.info'); const lines = []; for (building of buildings) { const id = building.getAttribute('data-building-id'); const status = data[`id_${id}`].status; const price = data[`id_${id}`].price; building.classList.add(`-${status}`); const line = new LeaderLine( LeaderLine.pointAnchor(building, { x: '50%', y: '50%' }), LeaderLine.pointAnchor(info, { x: '50%', y: 0 }), { color: '#fff', startPlug: 'arrow1', endPlug: 'behind', endSocket: 'top' } ); lines.push(line); } 

Como você pode ver, nada de complicado acontece. A linha possui "pontos de anexação" aos elementos. As coordenadas desses pontos em relação aos elementos geralmente são convenientes para determinar em porcentagem. Em geral, existem muitas opções diferentes, não faz sentido listar e lembrar, por isso recomendo apenas examinar a documentação. Uma dessas opções - startLabel - será necessária para criarmos uma pequena dica de ferramenta com um preço.


 const line = new LeaderLine( LeaderLine.pointAnchor(building, { x: '50%', y: '50%' }), LeaderLine.pointAnchor(info, { x: '50%', y: 0 }), { startLabel: LeaderLine.captionLabel(`${price}/`, { fontFamily: 'Rubik Mono One', fontWeight: 400, offset: [-30, -50], outlineColor: '#555' }), color: '#fff', startPlug: 'arrow1', endPlug: 'behind', endSocket: 'top', hide: true } ); 

Ninguém se preocupa em desenhar todas as dicas em um editor gráfico. Se eles deveriam ter um conteúdo consistente, pode até ser conveniente. Especialmente se houver um desejo de perguntar a eles posições diferentes para objetos diferentes.

Também podemos adicionar a opção ocultar para que todas as linhas não sejam exibidas como uma vassoura. Mostraremos um de cada vez, quando você passar o mouse sobre os prédios aos quais eles correspondem:


 building.addEventListener('mouseover', () => { line.show(); }); building.addEventListener('mouseout', () => { line.hide(); }); 

Aqui você pode exibir informações adicionais (no nosso caso, simplesmente o status atual do objeto) no local para obter informações. Acontecerá quase o necessário:



Essas coisas raramente são projetadas para dispositivos móveis, mas vale lembrar que elas geralmente são feitas em tela cheia na área de trabalho, e mesmo com alguns painéis ao lado para obter informações adicionais e você precisa esticar tudo muito bem. Algo assim, por exemplo:


 svg { width: 100%; height: 100%; } 

Nesse caso, as proporções do elemento SVG definitivamente não coincidem com as proporções da imagem dentro. O que fazer?


Proporções incompatíveis


A primeira coisa que vem à mente é a propriedade object-fit: cover do CSS. Mas há um ponto: ele absolutamente não é capaz de trabalhar com SVG. E mesmo que funcionasse, as casas ao longo das bordas do plano poderiam sair das bordas do esquema e tornar-se completamente inacessíveis. Então, aqui você precisa seguir um caminho um pouco mais complicado.


Primeiro passo O SVG possui um atributo preserveAspectRatio , que é um pouco semelhante à propriedade de ajuste do objeto (não exatamente, é claro, mas ...). Ao preserveAspectRatio="xMinYMin slice" para o elemento SVG principal do nosso plano, obtemos um circuito estendido sem espaços vazios nas bordas e sem distorção.


Etapa dois Você precisa arrastar e soltar com o mouse. Tecnicamente, ainda temos essa oportunidade. Aqui a tarefa é mais complicada, especialmente para iniciantes. Em teoria, temos eventos padrão para o mouse e a tela sensível ao toque que podem ser processados ​​e obtêm o valor de quanto o mapa precisa ser movido. Mas, na prática, você pode se envolver nisso por um período muito longo. O Hammer.js será uma alternativa - outra pequena biblioteca que leva toda a cozinha interna para si mesma e fornece uma interface simples para trabalhar com arrastar e soltar, deslizar, etc.


Precisamos mover a camada com os prédios e a imagem em todas as direções. Facilite:


 const buildingsLayer = map.querySelector('.buildings_layer'); const hammertime = new Hammer(buildingsLayer); hammertime.get('pan').set({ direction: Hammer.DIRECTION_ALL }); 

Por padrão, o hammer.js também inclui o reconhecimento de svaypas, mas não precisamos deles no mapa, portanto, desative-os imediatamente para não enganar nossas cabeças:


 hammertime.get('swipe').set({ enable: false }); 

Agora você precisa entender de alguma forma o que exatamente precisa postar para mover o mapa apenas para as bordas, mas não para mais longe. Com uma representação simples de dois retângulos em nossa cabeça, entendemos que, para isso, precisamos descobrir a indentação da camada com construções do elemento pai (SVG no nosso caso) dos quatro lados. GetBoundingClientRect vem ao resgate:


 const layer = buildingsLayer.getBoundingClientRect(); const parent = svg.getBoundingClientRect(); const offsets = { top: layer.top - parent.top, bottom: layer.bottom - parent.bottom, right: layer.right - parent.right, left: layer.left - parent.left, }; 

E por que ainda não temos mais maneira civilizada (e estável) de fazer isso? Puxar getBoundingClientRect toda vez é muito ruim em termos de desempenho, mas a escolha não é muito rica e é quase impossível perceber a inibição; portanto, não apresentaremos otimizações prematuras, onde tudo funciona bem. De uma maneira ou de outra, isso nos permite verificar a posição da camada com os edifícios e mover tudo apenas se fizer sentido:


 let translateX = 0; let translateY = 0; hammertime.on('pan', (e) => { const layer = buildingsLayer.getBoundingClientRect(); const parent = svg.getBoundingClientRect(); const offsets = { top: layer.top - parent.top, bottom: layer.bottom - parent.bottom, right: layer.right - parent.right, left: layer.left - parent.left, }; const speedX = e.velocityX * 10; const speedY = e.velocityY * 10; if (speedX > 0 && offsets.left < 0) { //  } else if (speedX < 0 && offsets.right > 0) { //  } if (speedY > 0 && offsets.top < 0) { //  } else if (speedY < 0 && offsets.bottom > 0) { //   } buildingsLayer.setAttribute('transform', `translate(${translateX} ${translateY})`); }); 

Nas bordas, geralmente vale a pena desacelerar para que não haja paradas ou empurrões repentinos. Assim, tudo vai e volta para algo como isto:


 if (speedX < -offsets.left) { translateX += speedX; } else { translateX += -offsets.left * speedX / 10; } 

Existem muitas opções para desacelerações. Este é o mais fácil. E sim, não é muito bonito, mas é burro como uma rolha e claro. Os coeficientes nesses exemplos geralmente são selecionados a olho nu, dependendo do comportamento desejado do cartão.


Se você abrir um navegador e brincar com o tamanho da janela nas ferramentas do desenvolvedor, poderá descobrir que algo deu errado ...


Forças impuras


Nos dispositivos de mesa, tudo funciona, mas no celular, a mágica acontece, ou seja, em vez de mover o mapa, o elemento do corpo se move. Oooooooo! Apenas o elenco não é suficiente. Apesar de tudo bem, isso acontece porque algo está transbordando em algum lugar e alguma substituição não foi configurada para transbordar: oculta. Mas, no nosso caso, pode acontecer que nada se mova.


Um enigma para os tipógrafos verdes: existe o elemento g, dentro do elemento svg, dentro do elemento div, dentro do elemento body, dentro do elemento html. Doctype naturalmente html. Se você adicionar transform: translate (...) a ele para arrastar o elemento g, no laptop ele se moverá como pretendido, mas no telefone nem se moverá. Não há erros no console. Mas há definitivamente um bug. O navegador é o último Chrome, lá e ali. A questão é por quê?

Sugiro que você pense 10 minutos sem o Google antes de assistir a resposta.



A resposta

Haha Eu te enganei. Mais precisamente, não é assim. Descrevi o que observaríamos com o teste manual. Mas, na realidade, tudo funciona como deveria. Isso não é um bug, mas um recurso relacionado à propriedade CSS da ação de toque . No contexto de nossa tarefa (de repente!), É revelado que existe e, além disso, tem um certo valor que quebra toda a lógica da interação com o mapa. Então, lidamos com ele de maneira muito rude:


 svg { touch-action: none !important; } 

Mas voltando às nossas ovelhas e veja o resultado (é melhor, é claro, abrir em uma guia separada):



Decidi não personalizar o código para nenhuma das estruturas da moda, para que ele permaneça na forma de um espaço em branco neutro sem forma, a partir do qual você poderá criar ao criar seus componentes.


Qual é o resultado?


Tendo passado bastante tempo, fizemos um plano no qual há uma imagem raster, destacando seus vários detalhes, conectando objetos não relacionados com setas e reações ao mouse. Espero ter conseguido transmitir a ideia básica de como tudo isso é feito na versão "orçamento". Como observamos no início do artigo, existem muitas aplicações diferentes, incluindo aquelas que não estão relacionadas a alguns sites de design confusos (embora essa abordagem seja usada com muita frequência neles). Bem, se você está procurando algo para ler sobre coisas interativas, mas já tridimensionais, deixo um link para um artigo sobre o tópico - Apresentações tridimensionais de produtos no Three.js, para os menores .

Source: https://habr.com/ru/post/pt478698/


All Articles