>웹 프론트엔드 >JS 튜토리얼 >[ 무작위 소프트웨어 프로젝트: dev.to Frontend 결론

[ 무작위 소프트웨어 프로젝트: dev.to Frontend 결론

Susan Sarandon
Susan Sarandon원래의
2024-10-10 06:17:02583검색

もう終わりだよ、おい!

かつての dev.to フロントエンドの課題に関する 3 部構成の最終エピソードに到達しました。かなり気が散ってしまいましたね!チャレンジそのものに応募するには遅すぎますが、私たちはその過程でいくつかの良い教訓を学び、きちんとした小さなライブラリも公開しました。しかし、今はそれを終わらせる時です。

今日は主に 2 つの大きなことに焦点を当てます。1 つは、太陽惑星ライブラリを統合して、惑星の実際の 3D 位置をリアルタイムで計算すること、もう 1 つは、個々の惑星のオンクリック イベントを介して、元のチャレンジからのマークアップの統合を完了することです。メッシュ。

太陽系惑星の統合

第 2 部で、私の頼りになるアストロ テキストの 1 つからの便利なアルゴリズムに基づいて、まともな小さな太陽惑星ライブラリをまとめたことを覚えているかもしれません。それ以来、簡単にアクセスできるように、基本的な糸パブリッシュを使用してこれを npmjs.org に掲載しました。今では独自のプロジェクト ページもあります。かっこいい。

[ Random Software Project: dev.to Frontend Conclusion

基本的な糸から始めて太陽惑星を追加し、パート 1 で中断したところから始めます。次に、yarn run dev を呼び出して、アプリのライブバージョンを実行します。これで、使用する準備が整いました!

惑星の位置を計算する

ライブラリをインポートした後、各惑星の関数呼び出しに渡す惑星軌道テーブルのデータ (または「カタログ」) も必要になります。私たちはこれを、掲載された元の論文にちなんで「スタンディッシュカタログ」と呼びました。これをコピーしてアプリのリソースとしてロードし (現時点では生のインポートだけで問題ありません)、アプリケーションのロード時に解析します。

これで、「プラネット ファクトリ」 (buildPlanet()) を更新して、「name」パラメータを含めることができます (これにより、このカタログと関連付けることができます)。次に、一致するものが見つかった場合は、ライブラリの getRvFromElementsDatetime() メソッドを使用して、そのメッシュに割り当てる位置を決定します。

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

この window.app.now Date オブジェクトも追加しますが、より一貫した時間モデリングに使用できる新しい THREE.Clock 機能もあります。これらの位置を割り当てると (もちろん、ウィンドウの読み込み時にカタログ JSON を解析することに加えて、このファクトリへの呼び出しを更新して新しい名前パラメーターも含めます)、惑星の位置を確認できるようになります。

[ Random Software Project: dev.to Frontend Conclusion

軌道跡のサンプリング

あなたが気づくかもしれないことの 1 つは、私たちの惑星は現在「実際の」位置を持っていますが、それらは私たちが線を作成した「軌道跡」と一致していないということです。これらは依然として「フラット」サークルとして実装されているため、整列しないのも不思議ではありません。今すぐ修正しましょう。

惑星の軌道軌跡を計算するには、同様の関数呼び出しを使用しますが、「現在」から将来の 1 公転周期までの時間をかけてサンプリングします。ただし、この公転周期は惑星ごとに異なるため、カタログを再度確認する必要があります。軌道トレースの「ファクトリー」にも「名前」パラメーターを追加して、それらのポイントがどのように計算されるかを詳しく見て更新しましょう。

まず、「均一な」サンプリングを時間にわたるサンプリングに置き換えます。引き続き固定数のポイントを使用しますが、「現在」から将来の 1 公転周期までのポイントを内挿します。カタログ モデルのエントリからこの「軌道周期」を計算するヘルパー メソッドを追加します。これは、以前の記事で実際に説明した非常に単純な方程式です。

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

次に、これを使用して buildOrbitTrace() 関数の for ループを変更します。

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

これで、3D 軌道跡が表示されるはずです。それらはわずかに傾いていて、わずかに歪んでいて、私たちの惑星の位置と一致しています。とてもクールです。

[ Random Software Project: dev.to Frontend Conclusion

Raycaster の衝突検出を終了する

パート 1 で終了したとき、シーン内のメッシュとの衝突を (各フレームで) 評価するためにレイキャスターを導入したばかりでした。簡単なアップデートが 1 つあります。それは、マウスの位置が正しく変換されていなかったということです。 y コンポーネントの計算に符号エラーがありました。代わりに次のようになります:

window.app.mouse_pos.y = -(event.clientY / window.innerHeight) * 2 + 1;

これを修正したので、これらの衝突が実際に発生していることをデバッグ/検証してみましょう。 (animate() 関数で) 交差が計算されると、それらの交差をハンドラー processCollision() に渡します。また、トリガーされる交差点の数もカウントするので、追加するマウスオーバー効果をいつ「リセット」するかが分かります。

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.

Trigger Mouseover Effects

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.

Trigger On-Click Dialog

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

Wrapping It Up

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!

[ Random Software Project: dev.to Frontend Conclusion

Cool stuff.

위 내용은 [ 무작위 소프트웨어 프로젝트: dev.to Frontend 결론의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.