3D地球儀500个标注小牌:DOM与キャンバス
0.先見问题
私のプロジェクトは、三次元地球 (WebGIS) 監視プラットフォームです。三次元シーン上の几几百个ラベル — 監視ポイント情報カード、機器ステータス アイコン、地理座標移動の追跡、ホバーのサポート、スプリング ウィンドウのクリック、アニメーション。
機能後のユーザーからのフィードバック:地球をドラッグするPPTさらに奇妙なのは——静止時はファンも回転します。
最終的な解決策は 1 つのスキルではなく、2 つのスキルによる完全な方法です。
インタラクティブな情報カード:DOMタブ + 脪方法,カメラが静止しているときにカメラをダウンロードします;
大型機器のアイコン:キャンバスtoDataURL広告塔となり、DOM電線から完全に離れます。
1. 選択タイプ:WebGL 里里性注二条路
3D エンジン里占 HTML マークアップには、当然 2 つのルートがあります:
| 「DOM」タブ | キャンバス贻图看板 | |
|---|---|---|
| 開発効率 | HTML + CSS | Canvas 2D API 手红一别 |
| 相互に | 原生ホバー/クリック | エンジン拾取(ピック)、セルフサービス接続 |
| 文字排版 | ブラウザは白 | 手動で |
| パフォーマンス | 受電影電影管線手紀 | 交给WebGL管線、天然快 |
我们的情報牌有图像展级(アイコン + 名前 + 警色边雷 + 呼吸アニメーション)、インタラクション要件は高く、DOM の操作を好む古いアーティスト、Canvas での DOM 功底——先选完了 DOM、価格はパフォーマンスです,そして、以下のような話がありました。
2.ポジショニング: 卡顿を最後まで卡在哪
Chromeのパフォーマンスパネルを開く Recalculate Style 和 Layout 妨害、画像はすべて電気効果にある性質のものです postRender コード例:
// 每帧、每个标签各执行一次
viewer.scene.postRender.addEventListener(() =>
const gradient = ctx.createLinearGradient(0, 0, 0, height);
const colorConfig = globalColorList[warningLevel] );
重要な知識ポイント:写す style 安いです(英语電影可设计脏,次フレーム統合重算))、高いのは「写後读」—— 刚写完スタイル即読 offsetWidth,被近内容标机最好(強制リフロー)を実行します。元のコードの順序は写→写→言→写であり、フレームごとの各タブはもう一度強制的にフローされます、几百个栠百个60fps下每秒上万次レイアウト計算。凶手ロック。
当時は気づかなかった問題もあります。それは位置決めです。 left/bottom、このタイプの属性はレイアウトをトリガーします。 「写後言」がない場合でも、1フレームあたり数百個のタブをドラッグ&ドロップしてください。また、完全なレイアウトを一度保存します。 transform: translate3d(x, y, 0)——小走合分器、非力レイアウト。 この改善は私たちの最終結果です。
「ファンがまだ動作している場合」についても説明します。postRender すべての力、カメラの非動作も常に正解です—すべてが全ライン稼働、CPU が全容量です。
顺带一提,エンジン レベルには、より完全なスイッチもあります。viewer.scene.requestRenderMode = true、シーンが変化するときのみ Cesiu をレンダリングさせ、シーンが静止しているときはレンダリング サイクルを停止することもできます。最終的にはチェックすることで問題を解決しましたが、シーン内に継続的なアニメーションがない場合は、このスイッチを最初に試す価値があります。これにより、ラベルの更新だけでなく、レンダリング サイクル全体が節約されます。
3. ルート 1 (上): N 个每很回调から 1 个まで
第一刀第正解:原原電影原设计设计 addEventListener、N 個の設定 = N 個の各再生、また、一遍市の個人バンドに変更されます。
// 标签管理器:共享帧回调
function sharedPostRender() {
if (labelRegistry.size === 0 || !sharedViewer || !sharedViewer.scene)
// 相机高度每帧只算一次(原先每个标签各算一次)
let cameraHeight;
try catch (err) {
return;
}
labelRegistry.forEach(label => {
try {
label.updateFrame(cameraHeight);
} catch (err) {
// 单个标签更新异常不影响其余标签
}
});
}
3 つの詳細:
- カメラの高さは、渡されたパラメータとして一度計算されます。——電影電影は技术安全電影の全線上红利;
- 単一のタグが例外 try-catch とともにスローされる—— そうしないと、1 つのタグのデータが壊れ、フレーム サイクル全体が中断され、すべてのタグが一緒にフリーズします。
destroy()DOM の登録と削除ができるようになると、リビルド後にシーンが取り残されることを防ぎます。
4. 英语一(中):脏方法——值没变正写
核心観察:見て見て見て地図の最高の時間カメラが動かないカメラは動かない → 投影は変わらない → 各フレームに書かれた値はすべて同じです。
// 屏幕坐标脏检查:与上一帧相同(相机静止)则跳过 DOM 写入
// 注意 scale 也在比对条件里——为什么,见坑三
const x = Math.round(this.windowPosition.x);
const y = Math.round(this.windowPosition.y);
if (x === this._lastX && y === this._lastY && s === this._lastScale) {
return;
}
this._lastX = x;
this._lastY = y;
this._lastScale = s;
カメラが静止しているとき、各フレーム DOM は ~4N 次降下から 0 まで書き込みます几行コード、三行坑:
坑一:不教整、脏方法永遠に不命中
投影計算の出力は浮動小数点です。カメラが「静止」している場合でも、フレーム間の浮動小数点も低下する可能性があります。 x 在 123.0000001 和 123.0000002 抖動の間——x === this._lastX 永遠の偽り、脏手机形同虚代。しなければならない Math.round 取る、電子ビデオを動かします。
(副作用としてタグ位置の画素精度が失われますが、情報カードクラスのタグには影響ありません。)
坑二:非表示はキャッシュする必要があります
设计有安全性制御(过太太远/被地理道性非表示時)display 状態が反転したときに 1 回だけ:
// 标签实例:可见性切换
_setVisible(visible) {
if (this._lastVisible === visible) {
return;
}
this._lastVisible = visible;
if (this.div) {
this.div.style.display = visible ? 'block' : 'none';
}
if (!visible) {
// 隐藏期间坐标可能变化,重置缓存以便重新显示时强制刷新
this._lastX = null;
this._lastY = null;
}
}
最後の 4 行に注目してください。キャッシュの座標が重置された場合に非表示にする nullこれは典型的な「最適化によるバグ」です。非表示中にユーザーが地球を回転すると、再度表示されたときにラベルの座標が変更されますが、内部検査のキャッシュは「変更なし」の結果と比較して古い値のままであり、直接書き込みをスキップします。タグ,ユーザーが再度カメラを移動するまで。
坑三:脏方法の粒度——鱗は飛ばせない
最初のバージョンは比对 x/y,命中就のみです return、技術のイメージも逆に実行されるように設計されています。ユーザーが特定のラベルをズームインすると、画面座標は変化せず、カメラの距離が変化します——x/y命中電気影電影、平移一下天才が実行するまでラベルは縮小しません。 これはまさに平移一下天才の特徴です。
ルールは 2 つの段落にアップグレードされます。脏正視题师按安全比机比对——x、y、各キャッシュ、各ジャッジをスケール、谕变了写谰;脏方法の整合性は、アクティブな失有路のすべてのパスが「書き込みをスキップすると失真の状態につながる可能性があります」でなければなりません—— Hideはそういう道です。
別のレイヤーを追加することもできますカメラクラスの検査:フレームごとに、カメラの位置と位置、フレーム全体がサイクル全体をスキップし、すべてのフレームがO(N)次投)罱次比对です。
5. 英语一(下):offsetWidth 一说一说
第 2 に戻る 节電影主凶——每堂读 offsetWidth 強制フローをトリガーします。
設定内容(アイコン + 名前)在图度次名可,幅一定,一度読了如:
// 标签实例:宽度缓存
_getLabelWidth() {
// 用 undefined 做哨兵而不是 !cachedWidth:宽度合法值可能是 0
// 且只在可见时测量——display:none 的元素 offsetWidth 恒为 0,测了也是错的
if (this._cachedWidth === undefined && this._isVisible()) {
this._cachedWidth = this.div.offsetWidth;
}
return this._cachedWidth;
}
最初に表示されると、一度測定されてキャッシュされ、その後、各フレームの読み取りが純粋なメモリ アクセスになります。
必須设计國际の设计注: ラベルの内容が動的に変更される場合 (たとえば、警告レベルの更新によりテキストの長さが変更される場合)、キャッシュが間違っているため、内容が変更されたときにキャッシュを自動的にクリアする必要があります。 _cachedWidth;同理、ウェブフォント、 必須 document.fonts.ready ラベルの内容は変更できません。フォントはフロント画面に表示され、完全なバージョンがロードされます。フォントはフロント画面に表示され、完了します。キャッシュの前提は「值高確率変化」ではなく「值台了有技术的天生机」。
この時点で、DOM ルートの最適化は完了です。ドラッグするとストリームに強制的に戻され、静止している場合はストリームが閉じられます。DOM ノード自体はまだ開発中です,スケールは再び 1 つの量レベルであり、命中正視吃滝主電影と組み合わせます。
6. ルート 2:Canvas 离画面結果 + toDataURL 贻图
思路の一文:握性注画成一图、交电影 贻图(看板)としての 3D エンジン、WebGL にすべてを処理させます—— ズームアウト、可視、カリング、ブロック、すべてエンジン本来の機能、毎回手書きする必要があるDOMの世界観はここには存在しません。
先代一选型问题:セシウム自带 LabelCollection(SDF 文字電視),パフォーマンスは優れていますが、スタイル機能はフォント、描画、背景色、描画不表示地変角色と電子効果に限定されます。 デバイスアイコンは「アイコン + テキスト + アラーム前色」の複合ビジョンを必要とするため、Canvas の自己描画を実行します。
6.1 描画:CSS の活儿曵電視干を握る
Canvas 上依次画四山: 時变圆角地址 → 文字 → 电影电影 → 电影線上:
// Canvas 绘制:渐变圆角背景
_drawLabelBackground(ctx, x, height, width, warningLevel) {
const gradient = ctx.createLinearGradient(0, 0, 0, height);
const colorConfig = globalColorList[warningLevel] || globalColorList[DEFAULT_WARNING_LEVEL];
gradient.addColorStop(0, colorConfig.start);
gradient.addColorStop(1, colorConfig.end);
ctx.fillStyle = gradient;
ctx.roundRect(x, 0, width, height, DEFAULT_RADIUS);
ctx.fill();
}
// Canvas 绘制:文字
_drawLabelText(ctx, text, canvasWidth, fontSize, warningLevel) {
ctx.font = `bold ${fontSize}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline="top";
ctx.fillStyle = globalTextColorList[warningLevel] || globalTextColorList[DEFAULT_WARNING_LEVEL];
ctx.shadowBlur = 0;
ctx.fillText(text, canvasWidth / 2, TEXT_Y_OFFSET);
}
DOM里一行 border-radius + linear-gradient 事、ここでは手動で変更することができ、手動でroundRectを実行します。利便性とパフォーマンスの交換は、コードの最初の行から始まりました。
プロセスの詳細:ctx.roundRect そうです新しい API (Chrome 99 以降 / Safari 16.4 以降)、古いブラウザではポリフィルまたはリトリートが必要です arcTo 手描きのパス——「英語语计ダウンロード」车车自己時でもキャンバスルートパス。
6.2 最初のもの: 文字英语歌词手机帮你算
DOM 行 offsetWidth 白送(这京贵),キャンバス里画画布在得先设计来来可以:
// 计算标签尺寸
const labelWidth = (finalConfig.fontSize + LABEL_WIDTH_OFFSET) * text.length;
const labelHeight = finalConfig.fontSize * LABEL_HEIGHT_MULTIPLIER;
(字号 + 2) × 字符数——これが中国語の文字の正しい捉え方です。 measureText 量宽——小は排版分における正確な幅(含文字分)、正確な幅(含文字分)のみが利用可能です font その後。終了時間 終了時間の電気的費用。
隠れた学費もあります。HiDPIキャンバスのCSSは、分割中に電気効果が発生し、網膜スクリーン上で膨張する可能性があります。 devicePixelRatio、 使用 ctx.scale 描画内容を拡大する、看板の scale DOM 英語では、電気ビデオのことについて、ここにさらにいくつか挙げます。
6.3 2 回目: 画像の読み込みは异步的、电影链会匠成金字塔
アイコンとインジケーターラインは2枚の絵ですが、drawImage しなければならない onload2 層の依存関係は 2 層の入れ子です。
// 图片加载嵌套链
deviceImg.onload = () => {
try {
...
lineImg.onload = () => {
try {
...
// 全部就绪才能创建实体
this._createEntity(canvas, posObj, targetData, deviceType, maxHeight, warningLevel);
} catch (error) { ... }
};
} catch (error) { ... }
};
各レイヤーが必要であることに注意してください onerror +トライキャッチ——画像が添付されている場合、このアイコンは消えます、これには外部のヘルメット底があります。 Promise.all 再構成され、ネストの上限は 2 層になります。
しかし回头見る,金字塔の根本は実際には各タブを個別に new Image() 同じバッチの写真をロードするより良い方法は、モジュール レベルのプリロードです。初期化時に、すべてのアイコンが完全にロードされ、サンプル イメージが共有され、その後、各描画が同期されます。 drawImage——安套链全旅行、比「容忍双山」高明交。
6.4 上の図:toDataURL と自動車生成によるこれらの電子効果
画完のキャンバス转成データURL,看板として贺图图片入业:
// 创建 billboard 实体
const base64 = canvas.toDataURL(IMAGE_FORMAT);
...
const entity = this.viewer.entities.add({
position: position,
targetData: targetData,
deviceType: deviceType,
show: true,
billboard: {
image: base64,
horizontalOrigin: this.Cesium.HorizontalOrigin.CENTER,
verticalOrigin: this.Cesium.VerticalOrigin.BOTTOM,
scaleByDistance: new this.Cesium.NearFarScalar(
SCALE_BY_DISTANCE_NEAR,
SCALE_BY_DISTANCE_NEAR_SCALE,
SCALE_BY_DISTANCE_FAR,
SCALE_BY_DISTANCE_FAR_SCALE
),
eyeOffset: new this.Cesium.Cartesian3(0, 0, 0),
scale: this._createScaleProperty(warningLevel),
distanceDisplayCondition: new this.Cesium.DistanceDisplayCondition(0, maxHeight)
// eyeOffset 用默认值 (0,0,0),无需显式传
}
});
この段落は 2 行です分水岭、逐次正解DOM電影の手写電影:
scaleByDistance: NearFarScalar(0, 1.0, 500, 0.3)——近傍原大、500米外0.3。distance / visibleHeight手書きの;distanceDisplayCondition: (0, maxHeight)ドム_setVisible註:方法管の;- 携帯カメラをフォローしてください——DOM部面里是解
postRender+ 投影 + 検査システム
WebGL ライン サイト デバイスはすべて、DOM ではなく、電子ビデオで生成されたものであり、推奨されるリプレイスです。 toDataURL 独自のコスト (同時に回读画布) がかかりますが、作成時に一度だけ実行されます—1 回限りのコストとフレームごとのコストは、パフォーマンスの意思決定において最も区別する価値のある 2 種類のコストです。
一度リプレイして、toDataURL 実際、それは最高ではありません: 看板 image Canvas 要素を直接送信できる、エンジンの自己処理アップロード——Base64 エンコード + デコード + もう一度 Base64 エンコード + デコード + もう一度 見見ます)、キャンバスを直接送信して再度見ます。
計算しなければならない紙もあります。メモリテクスチャ各キャンバス500個、200×60ピクセル、RGBA 4バイト約24MB、DPR 2下電子×4。キー按内容ダウンロードcanvas、ホモログ内容体育同一一贻图500 の企業は、テクスチャの記憶とストーリーの時間の両方を 1 つのレベルに上げることができます。
6.5 アニメーション:CallbackProperty——エンジンの「每很回调」内
CSS キーフレームはなく、エンジンの原文は次のとおりです。 CallbackProperty——每微回读你一值值:
// 呼吸动画:CallbackProperty
return new this.Cesium.CallbackProperty(function changeScale(time, result) {
const curTimestamp = Date.now();
if (curTimestamp - timestamp > ANIMATION_INTERVAL) {
speed += speedDelta;
curScale = speed ** 2;
timestamp = curTimestamp;
if (curScale >= MAX_SCALE) {
speedDelta *= -1;
} else if (curScale <= MIN_SCALE) {
speedDelta *= -1;
}
}
return curScale;
}, false);
電影眼熟吗?こちらはシングルフレームです——和DOM英語的 postRender ただし、内部の 200 ミリ秒のタイムスタンプ ストリームには、フル パフォーマンスの 200 ミリ秒を超える粒度が設定されており、この戻り値は WebGL レンダリングに入る粒度であることに注意してください。スタイルエラーをトリガーできません—— すべてのフレームが実行されるのと同じように、コストは 1 レベルです。
小天天:回调是電影里设计 time(JulianDate)、代わりに使用してください Date.now() エンジンクロックに近づけると、アニメーションがジャンプするときにシステム時間が調整されることも避けられます。
この印刷证第 2 节の设计:これらは「每很正視」ではなく、「每很此里性了 DOM」です。
6.6 互恵性: 拾取五生イベント
看板番号 addEventListener電影電視改方の拾い:相キャンバスの改→ scene.pick エンティティを取得 → 读挂在公司上海 targetData(参照 6.4 画像時間管理上海上天塞进了士生)。
下載随安全走,拾取即明是天生——これは Canvas 部里交流报線电影の正しい位置です、比安全一个”公司ID → 上流”の图像表倂干
6.7 Canvas電影のデザイン:電視手机
第 5 章では、DOM バッファのオンラインを置き換えます。ここでは、Canvas 電子効果を置き換えるショート パネルも入手します。内容不是电影,改支一重画一回電影電影電影性思惟(友友国警ビデオ配信)時,DOM 電影変更クラス完事,キャンバス路燍機canvas,再生成図,更新 billboard.image——キャッシュのコンテンツ レベルの 6.4 を実行した場合は、キャッシュのライフ サイクルを維持する必要もあります。
したがって、2 つのルートの適用可能な境界を 1 つのステップで再度調整できます。内容静的、大量 → Canvas; 内容高镇発行、小 → DOM当社の設備アイコンのステータス変更頻度は低く(2級/分級)、ヘビーデューティーなコスト分析で十分なCanvasルートの設定が可能です。
7. 効果とコントラスト
- DOMルート:時動時安全回流从 ~N次降下0; 静止時は全タグが全脏方法头最名合合帀回,帀回,帀雞,帀全タグ在脏民に設定されます
- キャンバスライン: 前期性
toDataURLコスト、実行時間 ゼロ DOM への参加、距離の短縮/削除/アニメーション化 完全に WebGL ライン内; - 2 つの設定が保存されると、情報カード + デバイス アイコンが百件の要求に対応し、ストリームが変更されます。静止していれば見える,メイン スレッドは空に近いです。Cesiu が各フレームをレンダリングしているとき、ビジョンには呼吸するアニメーションがあります。しかし、単一フレームのコストはミリ秒レベルからマイクロ秒レベルに上昇しています (パフォーマンス パネルで確認できます)。
自昺是是:パフォーマンス分录制最作電影 10 秒、最適化前エネルギー者密集的 Recalculate Style / Layout バンドル、最適化後のメイン電気は全空、再使用メモリ表面ハンドマシンは時間的 DOM を切り離さず、GPU テクスチャ メモリは時間的安全を持たない。
(待花:FPS、CPU占有、特性品表電影用量化合生——データ比形情*最形输、
8. 沉洉:四条可の原則
- 高长回调里,先電影再降镇——N 个電影监听汉机成 1 个上海安全電視,顺便成公共设计;
- 写電視做脏方法,读支持做设计—— スタイルを前に古い値と比較し、ジオメトリ属性を後で内部表示します。
- 最適化は次のパスを取るように設計する必要があります—— 非表示→重月要調整キャッシュ、コンテンツ変更要空キャッシュ幅。 キャッシュの失効メカニズムはなく、最適化は地雷ではありません。
- 先问「超不超在九记線」、再问「このパイプラインを最適化する方法」—— コンピュータは、DOM ルートの最適化を限界まで行い、Canvas ルートが不可能な場合もあります。
9. 結論
二条電子効果なし: 情報カードの相互作用は重い、少量、DOM + ハンドマシンの開発効率が最高; デバイス アイコンのスタイルはシンプル、大量、キャンバスの図のパフォーマンス天花杘。
特効薬はありません、あるのは妥協だけです——而して、エンジニアの価値、すべての妥協がスイッチであることはご存知でしょう。