Nous réalisons un plan de terrain interactif en 15 minutes


Sur le grille-pain, ils demandent souvent comment créer un diagramme interactif d'une maison, un plan de sa structure interne, la possibilité de sélectionner des étages ou des appartements avec des informations à leur sujet, d'afficher des informations sur les détails d'un produit particulier lorsque vous les survolez sur une photo, etc. Il ne s'agit pas d'un modèle en trois dimensions, mais d'une image avec la possibilité de mettre en évidence certains détails. Toutes ces tâches sont similaires et résolues assez simplement, mais néanmoins des questions continuent de se poser, alors aujourd'hui nous allons voir comment ces choses sont faites en utilisant SVG, un éditeur graphique et une pincée de javascript.


Le choix de SVG est dû au fait que c'est l'option la plus simple pour le développement et le débogage. J'ai rencontré des gens qui ont conseillé que tout cela soit fait sur toile, mais il est beaucoup plus difficile de comprendre ce qui se passe, et les coordonnées de tous les points sur les courbes doivent être calculées à l'avance d'une manière ou d'une autre, mais ici j'ai ouvert les outils du développeur et voir immédiatement toute la structure, tous les objets, avec lequel il y a interaction, et tout le reste peut être cliqué avec une souris dans une interface conviviale. Les performances entre la toile 2D habituelle et SVG ne différeront guère. WebGL peut donner un certain bonus à cet égard, mais le calendrier de développement augmentera considérablement, sans parler d'un soutien supplémentaire, qui ne correspond pas toujours au budget. Je dirais même «ne va jamais».


Dans ce didacticiel, nous allons faire quelque chose comme un widget pour un site fictif pour louer des maisons privées dans une certaine zone, mais il est clair que les principes de création de ces choses en question s'appliquent à n'importe quel sujet.


Pour commencer


Je vais tout montrer avec Inkscape comme exemple, mais toutes les mêmes actions peuvent être effectuées dans n'importe quel autre éditeur utilisant des fonctions similaires.


Pour fonctionner, nous avons besoin de deux boîtes de dialogue principales:


  • Éditeur XML (Ctrl + Maj + X ou une icône avec des crochets angulaires) - pour afficher la structure du document sous forme de balisage et modifier des éléments individuels.
  • Remplissage et contour (Ctrl + Maj + F ou l'icône avec un pinceau dans le cadre) - principalement pour remplir les contours.

Lancez-les immédiatement et procédez à la création du document.


Si vous les avez accidentellement glissés dans une fenêtre distincte, vous pouvez cliquer sous le cadre supérieur de cette fenêtre (là où il n'y a rien) et les faire glisser vers la fenêtre principale. Ce n'est pas entièrement intuitif, mais plutôt pratique.

Ouvrez une photo avec vue sur la région. Nous pouvons choisir d'insérer l'image elle-même sous la forme d'une chaîne base64 ou d'un lien externe. Puisqu'il est grand, sélectionnez le lien. Ensuite, nous changerons le chemin vers l'image avec nos mains lors de l'introduction de tout dans les pages du site. Un document SVG est créé dans lequel la photo sera intégrée via la balise d'image.


Pour les images raster incorporées dans SVG, incorporées dans HTML, il sera possible d'utiliser le chargement différé, ainsi que pour les images ordinaires sur les pages. Dans cet exemple, nous ne nous attarderons pas sur de telles optimisations, mais ne les oubliez pas dans les travaux pratiques.

Au stade actuel, nous voyons devant nous quelque chose comme ceci:



Créez maintenant un nouveau calque (Ctrl + Maj + N ou Menu> Calque> Ajouter un calque). Dans l'éditeur XML, nous voyons que l'élément g normal est apparu. Bien que nous ne soyons pas allés loin, nous pouvons lui donner une classe, que nous utiliserons plus tard dans les scripts.


Ne comptez pas sur id. Plus l'interface est complexe, plus il est facile de les faire se répéter et d'obtenir des bugs étranges. Et dans notre tâche, ils n'ont toujours aucun avantage. Les classes ou les attributs de données sont donc notre choix.

Si vous regardez de près la structure du document dans l'éditeur XML, vous remarquerez qu'il y a beaucoup de superflu. Tout éditeur de graphiques vectoriels plus ou moins complexe ajoutera quelque chose de propre aux documents. Pour supprimer tout cela avec vos mains est une tâche longue et ingrate, l'éditeur ajoutera constamment quelque chose à nouveau. Le nettoyage de SVG des ordures n'est donc effectué qu'à la fin du travail. Et de préférence sous une forme automatisée, car il existe des options toutes faites, le même svgo par exemple.


Trouvez un outil appelé Dessiner des courbes de Bézier et des lignes droites (Maj + F6). Avec lui, nous allons dessiner des contours fermés autour des objets. Dans notre tâche, nous devons décrire tous les bâtiments. Par exemple, nous nous limitons à six, mais en conditions réelles, il serait utile de pré-allouer du temps afin de définir avec précision tous les objets nécessaires. Bien qu'il arrive souvent qu'il existe de nombreuses entités similaires, les mêmes étages d'un bâtiment peuvent être absolument identiques. Dans de tels cas, vous pouvez accélérer un peu et copier-coller les courbes.


Après avoir encerclé les bâtiments nécessaires, nous revenons à l'éditeur XML, ajoutons des classes ou, très probablement, ce sera encore plus pratique, des attributs de données avec des index pour eux (c'est possible avec des adresses, mais comme nous avons une zone fictive, il n'y a que index) et déplacez tout vers le calque créé précédemment pour que tout soit «disposé sur des étagères». Et l'image, soit dit en passant, sera également utile pour s'y déplacer, afin que tout soit au même endroit, mais ce sont des bagatelles.


Maintenant, après avoir choisi un chemin - une courbe autour du bâtiment, vous pouvez tous les sélectionner avec Ctrl + A ou Menu> Édition> Tout sélectionner et éditer en même temps. Vous devez tous les peindre dans la fenêtre Remplissage et contour, et en même temps, supprimer là le trait supplémentaire. Eh bien, ou ajoutez-le si vous en avez besoin pour des raisons de conception.



Il est logique de peindre tous les contours avec une certaine couleur avec une valeur d'opacité minimale pour eux, même si cela n'est pas nécessaire en termes de conception. Le fait est que les navigateurs «intelligents» croient que vous ne pouvez pas cliquer sur un chemin vide, mais sur un chemin inondé - vous pouvez, même si personne ne voit ce remplissage.

Dans notre exemple, nous allons laisser une petite surbrillance en blanc pour mieux voir avec quels bâtiments nous travaillons, tout enregistrer et passer en douceur au navigateur et à l'éditeur de code plus familier.


Exemple de base


Créons une page html vide, collez le SVG résultant directement dedans et ajoutons du CSS pour que rien ne sorte de l'écran. Il n'y a rien à commenter.


.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; } 

Nous rappelons que nous avons ajouté des classes aux bâtiments et les utilisons pour que CSS soit plus ou moins structuré.


 .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; } 

Puisque nous avons défini des styles en ligne dans Inkscape, nous devons les interrompre en CSS. Serait-il plus pratique de tout faire en CSS? Oui et non. Cela dépend de la situation. Parfois, il n'y a pas de choix. Par exemple, si un designer a dessiné beaucoup de tout en couleurs et envelopper tout en CSS et le gonfler à l'impossibilité de ne pas en quelque sorte comme il faut. Dans cet exemple, j'utilise l'option «peu pratique» pour montrer qu'elle n'est pas particulièrement effrayante dans le contexte du problème résolu.



Supposons que nous ayons reçu de nouvelles données sur les maisons et ajoutez-leur différentes classes, en fonction de leur statut actuel:


 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}`); } 

Nous obtenons quelque chose comme ça:



Quelque chose de similaire à ce dont nous avons besoin est déjà visible. À ce stade, nous avons mis en évidence des objets sur le terrain qui répondent au survol de la souris. Et il n’est pas difficile d’ajouter pour eux une réponse à un clic de souris via l’addEventListener standard.


Ligne de leader


Souvent, il s'agit de créer des lignes qui relieront les objets sur la carte et certains éléments de la page avec des informations supplémentaires, ainsi que de faire des info-bulles minimales lors du survol de ces mêmes objets. Pour résoudre ces problèmes, la mini-bibliothèque de ligne de repère est très bien adaptée, ce qui crée des flèches vectorielles pour tous les goûts et toutes les couleurs.


Ajoutons les prix des info-bulles aux données et dessinons ces lignes.


 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); } 

Comme vous pouvez le voir, rien de compliqué ne se passe. La ligne a des «points d'attache» aux éléments. Les coordonnées de ces points par rapport aux éléments sont généralement pratiques à déterminer en pourcentage. En général, il existe de nombreuses options différentes, il n'est pas logique de répertorier et de mémoriser, je vous recommande donc de parcourir la documentation. L'une de ces options - startLabel - nous sera nécessaire pour créer une petite info-bulle avec un prix.


 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 } ); 

Personne ne se soucie de dessiner toutes les astuces dans un éditeur graphique. S'ils sont censés avoir un contenu cohérent, cela peut même être pratique. Surtout s'il y a un désir de leur demander différentes positions pour différents objets.

Nous pouvons également ajouter l'option masquer afin que toutes les lignes ne soient pas affichées comme un balai. Nous leur montrerons un à la fois lorsque vous survolerez les bâtiments auxquels ils correspondent:


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

Ici, vous pouvez afficher des informations supplémentaires (dans notre cas, simplement l'état actuel de l'objet) à la place des informations. Il s'avérera presque ce qui est nécessaire:



De telles choses sont rarement conçues pour les appareils mobiles, mais il convient de se rappeler qu'elles sont souvent affichées en plein écran sur le bureau, et même avec certains panneaux sur le côté pour des informations supplémentaires et vous devez tout étirer magnifiquement. Quelque chose comme ça par exemple:


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

Dans ce cas, les proportions de l'élément SVG ne coïncideront certainement pas avec les proportions de l'image à l'intérieur. Que faire?


Des proportions inégales


La première chose qui me vient à l'esprit est la propriété object-fit: cover de CSS. Mais il y a un point: il n'est absolument pas capable de fonctionner avec SVG. Et même si cela fonctionnait, les maisons le long des bords du plan pourraient sortir des bords du schéma et devenir complètement inaccessibles. Donc, ici, vous devez aller un peu plus compliqué.


Première étape. SVG a un attribut preserveAspectRatio , qui est quelque peu similaire à la propriété fit-fit (pas tout à fait, bien sûr, mais ...). En preserveAspectRatio="xMinYMin slice" pour l'élément SVG principal de notre plan, nous obtenons un circuit étendu sans vides sur les bords et sans distorsion.


Deuxième étape Vous devez faire un glisser-déposer avec la souris. Techniquement, nous avons encore une telle opportunité. Ici, la tâche est plus compliquée, surtout pour les débutants. En théorie, nous avons des événements standard pour la souris et l'écran tactile qui peuvent être traités et obtenir la valeur de la quantité de déplacement de la carte. Mais dans la pratique, vous pouvez vous y retrouver très longtemps. Hammer.js viendra à la rescousse - une autre petite bibliothèque qui prend toute la cuisine interne sur elle-même et fournit une interface simple pour travailler par glisser-déposer, glisser, etc.


Nous devons déplacer le calque avec les bâtiments et l'image dans toutes les directions. Rendez-le facile:


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

Par défaut, hammer.js inclut également la reconnaissance des svaypas, mais nous n'en avons pas besoin sur la carte, alors désactivez-les tout de suite afin de ne pas nous tromper:


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

Vous devez maintenant comprendre en quelque sorte exactement ce que vous devez publier pour déplacer la carte uniquement vers ses bords, mais pas plus loin. Avec une simple représentation de deux rectangles dans notre tête, nous comprenons que pour cela, nous devons trouver l'indentation de la couche avec les bâtiments de l'élément parent (SVG dans notre cas) des quatre côtés. GetBoundingClientRect vient à la rescousse:


 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, }; 

Et pourquoi n'avons-nous toujours pas de façon plus civilisée (et stable) de procéder? Tirer getBoundingClientRect à chaque fois est très mauvais en termes de performances, mais le choix n'est pas très riche, et il est presque impossible de remarquer une inhibition, donc nous ne proposerons pas d'optimisations prématurées où tout fonctionne très bien. D'une manière ou d'une autre, cela nous permet de vérifier la position de la couche par rapport aux bâtiments et de tout déplacer uniquement si cela a du sens:


 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})`); }); 

Sur les bords, il vaut généralement la peine de ralentir pour éviter les arrêts brusques ou les secousses. Ainsi, tout va et vient en quelque chose comme ça:


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

Il existe de nombreuses options de ralentissement. Celui-ci est le plus simple. Et oui, ce n’est pas très beau, mais c’est stupide comme un bouchon et clair. Les coefficients de ces exemples sont généralement sélectionnés à l'œil nu, en fonction du comportement souhaité de la carte.


Si vous ouvrez un navigateur et jouez avec la taille de la fenêtre dans les outils du développeur, vous pouvez constater que quelque chose s'est mal passé ...


Forces impures


Sur les appareils de bureau, tout fonctionne, mais sur mobile, la magie opère, à savoir qu'au lieu de déplacer la carte, l'élément corporel se déplace. Oooooooo! Seul le casting n'y suffit pas. Bien que ce soit correct, cela se produit car quelque chose déborde quelque part et certains écrasements n'ont pas été définis comme débordement: caché. Mais dans notre cas, il peut arriver que rien ne bouge du tout.


Une énigme pour les typographes verts: il y a l'élément g, à l'intérieur de l'élément svg, à l'intérieur de l'élément div, à l'intérieur de l'élément body, à l'intérieur de l'élément html. Doctype naturellement html. Si vous y ajoutez transform: translate (...) pour faire glisser l'élément g, alors sur l'ordinateur portable il se déplacera, comme prévu, mais sur le téléphone il ne bougera même pas. Il n'y a aucune erreur dans la console. Mais il y a certainement un bug. Le navigateur est le dernier Chrome à la fois là et là. La question est pourquoi?

Je vous suggère de penser à environ 10 minutes sans Google avant de regarder la réponse.



La réponse

Haha Je t'ai trompé. Plus précisément, non. J'ai décrit ce que nous observerions avec des tests manuels. Mais en réalité, tout fonctionne comme il se doit. Ce n'est pas un bug, mais une fonctionnalité liée à la propriété CSS de touch-action . Dans le cadre de notre tâche (tout d'un coup!) Il se révèle qu'elle existe, et, en plus, a une certaine valeur qui rompt toute la logique d'interaction avec la carte. Nous traitons donc très grossièrement avec lui:


 svg { touch-action: none !important; } 

Mais revenons à nos moutons et regardons le résultat (il vaut mieux, bien sûr, ouvrir dans un onglet séparé):



J'ai décidé de ne personnaliser le code pour aucun des frameworks à la mode, afin qu'il reste sous la forme d'un vide neutre sans forme, à partir duquel vous pouvez vous appuyer lors de la création de vos composants.


Quel est le résultat?


Après avoir passé pas mal de temps, nous avons fait un plan sur lequel il y a une image raster, mettant en évidence ses divers détails, connectant des objets sans rapport avec des flèches et des réactions à la souris. J'espère avoir réussi à transmettre l'idée de base de la façon dont tout cela se fait dans la version «budget». Comme nous l'avons noté au début de l'article, il existe de nombreuses applications différentes, y compris celles qui ne sont pas liées à une sorte de sites de conception confus (bien que cette approche soit utilisée très souvent sur eux). Eh bien, si vous cherchez quelque chose à lire sur des choses interactives, mais déjà en trois dimensions, je laisse un lien vers un article sur le sujet - Présentations de produits en trois dimensions sur Three.js pour les plus petits .

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


All Articles