Nous avons atteint le dernier épisode de notre programme en trois parties sur ce qui était autrefois le défi frontend dev.to. Nous avons été assez distraits, n'est-ce pas !? Bien qu'il soit beaucoup trop tard pour relever le défi lui-même, nous avons tiré de bonnes leçons en cours de route et avons même publié une jolie petite bibliothèque. Mais il est maintenant temps de conclure.
Nous allons nous concentrer principalement sur deux grandes choses aujourd'hui : intégrer notre bibliothèque de planètes solaires pour calculer les positions 3D réelles de nos planètes en temps réel, et terminer l'intégration du balisage du défi original via des événements en clic pour chaque planète. mailles.
Vous vous souviendrez peut-être de la deuxième partie que nous avions constitué une petite bibliothèque décente de planètes solaires, basée sur un algorithme utile issu de l'un de mes textes astro incontournables. Depuis, nous avons utilisé une publication de base sur le fil pour publier ceci sur npmjs.org pour un accès facile ; il a même sa propre page de projet maintenant ! Cool.
Nous pouvons commencer avec un fil de base, ajouter des planètes solaires, puis, là où nous nous sommes arrêtés dans la première partie. Ensuite, nous pouvons appeler Yarn Run Dev pour lancer une version live de notre application. Et maintenant nous sommes prêts à l'utiliser !
Après avoir importé la bibliothèque, nous aurons également besoin des données (ou "catalogue") des tables d'orbites planétaires à transmettre à l'appel de fonction pour chaque planète. Nous avons appelé cela le « catalogue standish » d'après le journal original dans lequel il a été publié. Nous allons le copier pour le charger en tant que ressource dans notre application (une simple importation brute pour l'instant suffit), puis l'analyser lors du chargement de l'application.
Maintenant, nous pouvons mettre à jour notre "usine planétaire" (buildPlanet()) pour inclure un paramètre "nom" (afin que nous puissions établir une corrélation avec ce catalogue). Ensuite, nous utiliserons la méthode getRvFromElementsDatetime() de notre bibliothèque, si une correspondance est trouvée, pour déterminer la position que nous attribuons à ce maillage.
const poe = window.app.catalog(hasOwnProperty(name.toLowerCase()) ? window.app.catalog[name.toLowerCase()] : null; if (pow) { const [r, _] = solarplanets.getRvFromElementsDatetime(poe, window.app.now); mesh.position.set( spatialscale * r[0], spatialscale * r[1], spatialscale * r[2] ); }
Nous ajouterons également cet objet window.app.now Date, bien qu'il existe également une fonctionnalité THREE.Clock plus récente que nous pourrions utiliser pour une modélisation temporelle plus cohérente. Et une fois que nous avons attribué ces positions (en mettant à jour l'appel à cette usine pour inclure également le nouveau paramètre de nom, bien sûr, en plus d'analyser le catalogue JSON au chargement de la fenêtre), nous pouvons maintenant voir nos positions planétaires !
Une chose que vous remarquerez peut-être est que, même si nos planètes ont désormais des positions « réelles », elles ne correspondent pas aux « traces orbitales » pour lesquelles nous avions produit des lignes. Ceux-ci sont toujours implémentés sous forme de cercles « plats », il n’est donc pas surprenant qu’ils ne s’alignent pas. Réparons ça maintenant.
Pour calculer la trace de l'orbite d'une planète, nous utiliserons un appel de fonction similaire, mais nous échantillonnerons au fil du temps à partir de "maintenant" jusqu'à une période orbitale dans le futur. Cette période orbitale, cependant, est différente pour chaque planète, nous devrons donc revoir notre catalogue. Ajoutons également un paramètre "nom" à "l'usine" de trace d'orbite, puis plongeons-nous et mettons à jour la façon dont ces points sont calculés.
Tout d'abord, nous remplacerons l'échantillonnage "uniforme" par un échantillonnage dans le temps. Nous utiliserons toujours un nombre fixe de points, mais nous les interpolerons à partir de l'heure « actuelle » vers une période orbitale dans le futur. Nous ajouterons une méthode d'assistance pour calculer cette "période orbitale" à partir de l'entrée du modèle de catalogue, qui est une équation assez simple que nous avons examinée dans les articles précédents.
function getPeriodFromPoe(poe) { const MU_SUN_K3PS2 = 1.327e11; const au2km = 1.49557871e8; return 2 * Math.PI * Math.sqrt(Math.pow(poe.a_au * au2km, 3.0) / MU_SUN_K3PS2); }
Nous allons maintenant utiliser ceci pour modifier la boucle for dans la fonction buildOrbitTrace().
const poe = window.app.catalog.hasOwnProperty(name.toLowerCase()) ? window.app.catalog[name.toLowerCase()] : null; if (pow) { const T0 = window.app.now; const T_s = getPeriodFromPoe(poe); for (var i = 0; i < (n + 1); i += 1) { const t_dt = new Date(T0.valueOf() + T_s * 1e3 * i / n); const [r, _] = solarplanets.getRvFromElementsDatetime(poe, t_dt); points.push(new THREE.Vector3( spatialScale * r[0], spatialScale * r[1], spatialScale * r[2] )); } }
Et avec cela, vous devriez maintenant voir des traces d'orbite en 3D ! Ils sont légèrement inclinés, légèrement asymétriques et alignés avec les positions de notre planète. Plutôt cool.
Quand nous avons arrêté la première partie, nous venions d'installer un raycaster pour évaluer (sur chaque image) les collisions avec les maillages de notre scène. J'ai une brève mise à jour, à savoir que la position de la souris n'était pas traduite correctement. Il y a eu une erreur de signe dans le calcul de la composante y, qui devrait plutôt ressembler à ceci :
window.app.mouse_pos.y = -(event.clientY / window.innerHeight) * 2 + 1;
Une fois cela corrigé, déboguons/vérifions que ces collisions ont réellement lieu. Lorsque les intersections sont calculées (dans notre fonction animate()), nous allons transmettre ces intersections à un gestionnaire, processCollision(). Nous allons également compter le nombre d'intersections déclenchées, afin de savoir quand "réinitialiser" les effets de survol que nous allons ajouter.
let nTriggered = 0; if (intersections.length > 0) { intersections.forEach(intersection => { nTriggered += processCollision(intersection) ? 1 : 0; }); } if (nTriggered === 0) { // reset stuff }
With that in mind, we're ready to write our handler. First, let's just check to make sure the mesh has a name. (Which will require, by the way, that we assign the planet name in the buildPlanet() factory to the mesh that is created. In this function, we will also need to set a scale for the mesh, like mesh.scale.set(2, 2, 2), to make collisions easier. Make sure you make these changes now.) If it has a name that matches a known planet, we'll log it to the console for now.
function processCollision(intersection) { const name = intersection.object.name; const names = planets.map(p => p.name.toLowerCase()); if (name !== "" && names.includes(name)) { console.log(name); return true; } return false; }
(Note we are normalizing names to lower case for comparison--always a good idea, just in case you lose track of consistent capitalization within the model data you are using.)
This should be enough for you to see (for example) "venus" should up in your console log when you move the mouse over Venus.
With intersections being computed correctly, let's add some effects. Even if the user doesn't click, we want to indicate (as a helpful hint) that the mouse is over something clickable--even though it's a mesh in a 3d scene, and not just a big button in a 2d UI. We're going to make two changes: the mesh will turn white, and the cursor will change to a "pointer" (the hand, instead of the default arrow).
In our processCollision() function, we'll replace the console.log() call with a change to the object material color, and change the body style of the document (a little cheating here...) to the appropriate style.
//mouseover effects: change planet color, mouse cursor intersection.object.material.color.set(0xffffff); window.document.body.style.pointer = "cursor"; return true;
We'll also need to use our collision check aggregation (nTriggered, in the animate() function) to reset these changes when the mouse is not hovering over a planet. We had an if block for this, let's populate it now:
if (nTriggered === 0) { // reset state window.document.body.style.cursor = "inherit"; planets.forEach(p => { const sgn = window.app.scene.getObjectByName(p.name.toLowerCase()); if (sgn) { sgn.material.color.set(p.approximateColor_hex); } }); }
Give it a spin! Hover over some planets and check out that deliciousness.
To satisfy the original intention (and requirements) of the challenge, we need to integrate the markup. We'll move all of this markup into our HTML, but tweak the style to hide it so the 3d canvas continues to take up our screen. Instead, when a planet is clicked, we'll query this markup to display the correct content in a quasi-dialog.
If you copy the entire body of the markup, you can tweak our top-level CSS to make sure it doesn't show up before a mouse button is pressed:
header, section, article, footer { display: none }
And from the console, you can verify that we can use query selector to grab a specific part of this markup:
window.document.body.querySelector("article.planet.neptune");
So, we're ready to implement our on-click event. But, we need to keep track of the mouse state--specifically, if the button is down. We'll add an is_mouse_down property to our module-scope window.app Object, and add some global handlers to our onWindowLoad() function that update it:
window.addEventListener("mousedown", onMouseDown); window.addEventListener("mouseup", onMouseUp);
The sole purpose of these event handlers is to update that window.app.is_mouse_down property so we'll be able to check it in our collision intersection handler. So, they're pretty simple:
function onMouseDown(event) { window.app.is_mouse_down = true; } function onMouseUp(event) { window.app.is_mouse_down = false; }
In a more formally designed application, we might encapsulate management (and lookup) of mouse state (and signals) in their own object. But this will work for our purposes. Now, we're ready to revisit the body of our processCollision() handler and add support for showing the dialog:
if (window.app.is_mouse_down) { dialogOpen(name); }
We'll add two handlers for dialog management: dialogOpen() and dialogClose(). The latter will be invoked when the mouse moves out of the mesh collision--in other words, exactly where we reset the other UI states, in animate():
if (nTriggered === 0) { // ... dialogClose(); }
Now we're ready to write these. Since we're not using any kind of UI library or framework, we'll manually hand-jam a basic
that we populate from the article content, then style as needed.function dialogOpen(name) { let dialog = window.document.body.querySelector(".Dialog"); if (!dialog) { dialog = window.document.createElement("div"); dialog.classList.add("Dialog"); window.document.body.appendChild(dialog); } // query article content to populate dialog const article = window.document.body.querySelector(`article.planet.${name}`); if (!article) { return; } dialog.innerHTML = article.innerHTML; dialog.style.display = "block"; }
A word of caution: Assigning innerHTML is a surprisingly expensive operation, even if you're just emptying it with a blank string! There's all sorts of parsing and deserialization the browser has to do under the hood, even if it is a method native to the DOM API itself. Be careful.
Conversly, the dialogClose() event is quite simple, since we just need to check to see if the dialog exists, and if it does, close it. We don't even need to know the name of the relevant planet.
function dialogClose() { const dialog = window.document.body.querySelector(".Dialog"); if (!dialog) { return; } dialog.style.display = "none"; }
And of course we'll want to add some modest styling for this dialog in our top-level CSS file for the application:
.Dialog { position: absolute; top: 0; left: 0; background-color: rgba(50, 50, 50, 0.5); color: #eee; margin: 1rem; padding: 1rem; font-family: sans-serif; border: 1px solid #777; border-radius: 1rem; }
Of course, we still only have the "inner" planets. We can easily add the "outer" planets just by augmenting our module-scope planets dictionary that is used to populate the scene. (Entries in the markup and the planet catalog already exist.)
// ... }, { "name": "Jupiter", "radius_km": 6.9911e4, "semiMajorAxis_km": 7.78479e8, "orbitalPeriod_days": 4332.59, "approximateColor_hex": 0xaa7722 }, { "name": "Saturn", "radius_km": 5.8232e4, "semiMajorAxis_km": 1.43353e9, "orbitalPeriod_days": 10755.7, "approximateColor_hex": 0xccaa55 }, { "name": "Uranus", "radius_km": 2.5362e4, "semiMajorAxis_km": 2.870972e9, "orbitalPeriod_days": 30688.5, "approximateColor_hex": 0x7777ff }, { "name": "Neptune", "radius_km": 2.4622e4, "semiMajorAxis_km": 4.50e9, "orbitalPeriod_days": 60195, "approximateColor_hex": 0x4422aa }
This is even enough for you to start doing some verification & validation when you walk outside tonight! Take a close look at how the planets are arranged. At midnight, you're on the far side of the earth from the sun. What planets should you be able to see, if you trace the path of the zodiac (or ecliptic) from east to west? Can you see Venus, and when? What's the relative arrangement of Jupiter and Saturn? You can predict it!
Cool stuff.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!