@jjlmoya/utils-drones 1.35.0 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +4 -0
  3. package/src/entries.ts +8 -0
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/drone-mission-battery-reserve-planner/bibliography.astro +6 -0
  8. package/src/tool/drone-mission-battery-reserve-planner/bibliography.ts +7 -0
  9. package/src/tool/drone-mission-battery-reserve-planner/component.astro +229 -0
  10. package/src/tool/drone-mission-battery-reserve-planner/controller.ts +124 -0
  11. package/src/tool/drone-mission-battery-reserve-planner/dom-views.ts +114 -0
  12. package/src/tool/drone-mission-battery-reserve-planner/drone-mission-battery-reserve-planner.css +392 -0
  13. package/src/tool/drone-mission-battery-reserve-planner/entry.ts +27 -0
  14. package/src/tool/drone-mission-battery-reserve-planner/evaluator.ts +23 -0
  15. package/src/tool/drone-mission-battery-reserve-planner/i18n/de.ts +223 -0
  16. package/src/tool/drone-mission-battery-reserve-planner/i18n/en.ts +223 -0
  17. package/src/tool/drone-mission-battery-reserve-planner/i18n/es.ts +223 -0
  18. package/src/tool/drone-mission-battery-reserve-planner/i18n/fr.ts +223 -0
  19. package/src/tool/drone-mission-battery-reserve-planner/i18n/id.ts +223 -0
  20. package/src/tool/drone-mission-battery-reserve-planner/i18n/it.ts +223 -0
  21. package/src/tool/drone-mission-battery-reserve-planner/i18n/ja.ts +223 -0
  22. package/src/tool/drone-mission-battery-reserve-planner/i18n/ko.ts +223 -0
  23. package/src/tool/drone-mission-battery-reserve-planner/i18n/nl.ts +223 -0
  24. package/src/tool/drone-mission-battery-reserve-planner/i18n/pl.ts +223 -0
  25. package/src/tool/drone-mission-battery-reserve-planner/i18n/pt.ts +223 -0
  26. package/src/tool/drone-mission-battery-reserve-planner/i18n/ru.ts +223 -0
  27. package/src/tool/drone-mission-battery-reserve-planner/i18n/sv.ts +223 -0
  28. package/src/tool/drone-mission-battery-reserve-planner/i18n/tr.ts +223 -0
  29. package/src/tool/drone-mission-battery-reserve-planner/i18n/zh.ts +223 -0
  30. package/src/tool/drone-mission-battery-reserve-planner/index.ts +11 -0
  31. package/src/tool/drone-mission-battery-reserve-planner/logic.test.ts +94 -0
  32. package/src/tool/drone-mission-battery-reserve-planner/logic.ts +203 -0
  33. package/src/tool/drone-mission-battery-reserve-planner/seo.astro +15 -0
  34. package/src/tool/drone-mission-battery-reserve-planner/storage.ts +59 -0
  35. package/src/tool/drone-mission-battery-reserve-planner/ui.ts +78 -0
  36. package/src/tool/fpv-drone-speed-calculator/bibliography.astro +6 -0
  37. package/src/tool/fpv-drone-speed-calculator/bibliography.ts +14 -0
  38. package/src/tool/fpv-drone-speed-calculator/component.astro +115 -0
  39. package/src/tool/fpv-drone-speed-calculator/controller.ts +105 -0
  40. package/src/tool/fpv-drone-speed-calculator/dom-views.ts +80 -0
  41. package/src/tool/fpv-drone-speed-calculator/entry.ts +27 -0
  42. package/src/tool/fpv-drone-speed-calculator/fpv-drone-speed-calculator.css +558 -0
  43. package/src/tool/fpv-drone-speed-calculator/i18n/de.ts +20 -0
  44. package/src/tool/fpv-drone-speed-calculator/i18n/en.ts +47 -0
  45. package/src/tool/fpv-drone-speed-calculator/i18n/es.ts +40 -0
  46. package/src/tool/fpv-drone-speed-calculator/i18n/fr.ts +13 -0
  47. package/src/tool/fpv-drone-speed-calculator/i18n/id.ts +13 -0
  48. package/src/tool/fpv-drone-speed-calculator/i18n/it.ts +13 -0
  49. package/src/tool/fpv-drone-speed-calculator/i18n/ja.ts +13 -0
  50. package/src/tool/fpv-drone-speed-calculator/i18n/ko.ts +13 -0
  51. package/src/tool/fpv-drone-speed-calculator/i18n/nl.ts +13 -0
  52. package/src/tool/fpv-drone-speed-calculator/i18n/pl.ts +13 -0
  53. package/src/tool/fpv-drone-speed-calculator/i18n/pt.ts +13 -0
  54. package/src/tool/fpv-drone-speed-calculator/i18n/ru.ts +13 -0
  55. package/src/tool/fpv-drone-speed-calculator/i18n/sv.ts +13 -0
  56. package/src/tool/fpv-drone-speed-calculator/i18n/tr.ts +13 -0
  57. package/src/tool/fpv-drone-speed-calculator/i18n/zh.ts +13 -0
  58. package/src/tool/fpv-drone-speed-calculator/index.ts +11 -0
  59. package/src/tool/fpv-drone-speed-calculator/logic.test.ts +53 -0
  60. package/src/tool/fpv-drone-speed-calculator/logic.ts +126 -0
  61. package/src/tool/fpv-drone-speed-calculator/seo.astro +16 -0
  62. package/src/tool/fpv-drone-speed-calculator/ui.ts +47 -0
  63. package/src/tools.ts +7 -0
@@ -0,0 +1,80 @@
1
+ import type { FpvDroneSpeedResults } from './logic';
2
+ import type { FpvDroneSpeedUI } from './ui';
3
+
4
+ export type DisplayUnit = 'metric' | 'imperial';
5
+
6
+ interface RenderContext {
7
+ root: HTMLElement;
8
+ results: FpvDroneSpeedResults;
9
+ ui: FpvDroneSpeedUI;
10
+ unit: DisplayUnit;
11
+ }
12
+
13
+ function number(value: number, digits = 0): string {
14
+ return new Intl.NumberFormat(undefined, { maximumFractionDigits: digits }).format(value);
15
+ }
16
+
17
+ function speed(value: number, unit: DisplayUnit): string {
18
+ return `${number(unit === 'metric' ? value : value / 1.60934)} ${unit === 'metric' ? 'km/h' : 'mph'}`;
19
+ }
20
+
21
+ function mass(value: number, unit: DisplayUnit): string {
22
+ return `${number(unit === 'metric' ? value : value / 28.3495, unit === 'metric' ? 0 : 1)} ${unit === 'metric' ? 'g' : 'oz'}`;
23
+ }
24
+
25
+ function setText(root: HTMLElement, key: string, value: string): void {
26
+ root.querySelectorAll<HTMLElement>(`[data-output="${key}"]`).forEach((target) => {
27
+ target.textContent = value;
28
+ });
29
+ }
30
+
31
+ function diagnosisText(results: FpvDroneSpeedResults, ui: FpvDroneSpeedUI): [string, string] {
32
+ if (results.diagnosis === 'overspeed') return [ui.diagnosisOverspeed, ui.diagnosisOverspeedAdvice];
33
+ if (results.diagnosis === 'high-slip') return [ui.diagnosisHighSlip, ui.diagnosisHighSlipAdvice];
34
+ if (results.diagnosis === 'heavy-load') return [ui.diagnosisHeavyLoad, ui.diagnosisHeavyLoadAdvice];
35
+ return [ui.diagnosisPlanning, ui.diagnosisPlanningAdvice];
36
+ }
37
+
38
+ export function renderResults({ root, results, ui, unit }: RenderContext): void {
39
+ const [diagnosis, advice] = diagnosisText(results, ui);
40
+ setText(root, 'estimated-speed', speed(results.estimatedSpeedKmh, unit));
41
+ setText(root, 'pitch-speed', speed(results.pitchSpeedKmh, unit));
42
+ setText(root, 'loaded-rpm', `${number(results.loadedRpm)} rpm`);
43
+ setText(root, 'no-load-rpm', `${number(results.noLoadRpm)} rpm`);
44
+ setText(root, 'slip', `${number(results.slipPercent, 1)}%`);
45
+ setText(root, 'load-effect', `${number(results.loadPenaltyPercent, 1)}%`);
46
+ setText(root, 'speed-gap', speed(Math.max(0, results.pitchSpeedKmh - results.estimatedSpeedKmh), unit));
47
+ setText(root, 'speed-axis-start', speed(0, unit));
48
+ setText(root, 'speed-axis-end', speed(300, unit));
49
+ setText(root, 'diagnosis', diagnosis);
50
+ setText(root, 'diagnosis-advice', advice);
51
+ results.sensitivity.forEach((point) => {
52
+ setText(root, `${point.label}-pitch`, `${number(point.pitchInches, 1)} in`);
53
+ setText(root, `${point.label}-speed`, speed(point.speedKmh, unit));
54
+ const bar = root.querySelector<HTMLElement>(`[data-bar="${point.label}"]`);
55
+ if (bar) bar.style.setProperty('--bar-width', `${Math.min(100, Math.max(4, point.speedKmh / Math.max(results.sensitivity[2]?.speedKmh ?? 1, 1) * 100))}%`);
56
+ });
57
+ root.dataset.diagnosis = results.diagnosis;
58
+ const speedPosition = Math.min(92, Math.max(8, results.estimatedSpeedKmh / 300 * 84 + 8));
59
+ const pitchPosition = Math.min(92, Math.max(8, results.pitchSpeedKmh / 300 * 84 + 8));
60
+ root.style.setProperty('--speed-position', `${speedPosition}%`);
61
+ root.style.setProperty('--pitch-position', `${pitchPosition}%`);
62
+ }
63
+
64
+ export function updateUnitLabels(root: HTMLElement, unit: DisplayUnit, ui: FpvDroneSpeedUI): void {
65
+ root.querySelectorAll<HTMLElement>('[data-unit-label="mass"]').forEach((element) => { element.textContent = unit === 'metric' ? 'g' : 'oz'; });
66
+ root.querySelectorAll<HTMLElement>('[data-unit-label="speed"]').forEach((element) => { element.textContent = unit === 'metric' ? ui.speedUnit : 'mph'; });
67
+ }
68
+
69
+ export function formatMassInput(grams: number, unit: DisplayUnit): string {
70
+ return unit === 'metric' ? String(Math.round(grams)) : String(Math.round(grams / 28.3495 * 10) / 10);
71
+ }
72
+
73
+ export function parseMassInput(value: string, unit: DisplayUnit): number {
74
+ const parsed = Number(value);
75
+ return unit === 'metric' ? parsed : parsed * 28.3495;
76
+ }
77
+
78
+ export function formatMassLabel(grams: number, unit: DisplayUnit): string {
79
+ return mass(grams, unit);
80
+ }
@@ -0,0 +1,27 @@
1
+ import type { DronesToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { FpvDroneSpeedUI } from './ui';
3
+
4
+ export type { FpvDroneSpeedUI };
5
+ export type FpvDroneSpeedLocaleContent = ToolLocaleContent<FpvDroneSpeedUI>;
6
+
7
+ export const fpvDroneSpeedCalculator: DronesToolEntry<FpvDroneSpeedUI> = {
8
+ id: 'fpv-drone-speed-calculator',
9
+ icons: { bg: 'mdi:quadcopter', fg: 'mdi:speedometer' },
10
+ i18n: {
11
+ es: () => import('./i18n/es').then((module) => module.content),
12
+ en: () => import('./i18n/en').then((module) => module.content),
13
+ fr: () => import('./i18n/fr').then((module) => module.content),
14
+ de: () => import('./i18n/de').then((module) => module.content),
15
+ it: () => import('./i18n/it').then((module) => module.content),
16
+ pt: () => import('./i18n/pt').then((module) => module.content),
17
+ nl: () => import('./i18n/nl').then((module) => module.content),
18
+ pl: () => import('./i18n/pl').then((module) => module.content),
19
+ ru: () => import('./i18n/ru').then((module) => module.content),
20
+ ja: () => import('./i18n/ja').then((module) => module.content),
21
+ ko: () => import('./i18n/ko').then((module) => module.content),
22
+ zh: () => import('./i18n/zh').then((module) => module.content),
23
+ tr: () => import('./i18n/tr').then((module) => module.content),
24
+ sv: () => import('./i18n/sv').then((module) => module.content),
25
+ id: () => import('./i18n/id').then((module) => module.content),
26
+ },
27
+ };
@@ -0,0 +1,558 @@
1
+ :root {
2
+ --speed-ink: #14211f;
3
+ --speed-muted: #60706d;
4
+ --speed-paper: #f3f1e8;
5
+ --speed-card: #fffdf5;
6
+ --speed-line: #d8d8c8;
7
+ --speed-teal: #006b68;
8
+ --speed-lime: #c7f36d;
9
+ --speed-coral: #ee745a;
10
+ --speed-slate: #1f3333;
11
+ --speed-shadow: rgba(20, 33, 31, .14);
12
+ }
13
+
14
+ .theme-dark .fpv-speed-tool {
15
+ --speed-ink: #edf5ee;
16
+ --speed-muted: #a7b6b0;
17
+ --speed-paper: #17221f;
18
+ --speed-card: #1e2d29;
19
+ --speed-line: #3a4c46;
20
+ --speed-teal: #61d1c1;
21
+ --speed-lime: #d5f67f;
22
+ --speed-coral: #ff9e80;
23
+ --speed-slate: #10201d;
24
+ --speed-shadow: rgba(0, 0, 0, .28);
25
+ }
26
+
27
+ .fpv-speed-tool {
28
+ overflow: hidden;
29
+ border: 1px solid var(--speed-line);
30
+ border-radius: 1.5rem;
31
+ background: var(--speed-card);
32
+ box-shadow: 0 1.4rem 3.5rem var(--speed-shadow);
33
+ color: var(--speed-ink);
34
+ }
35
+
36
+ .speed-brief,
37
+ .flight-deck,
38
+ .assumptions-row {
39
+ padding: clamp(1rem, 3vw, 2rem);
40
+ }
41
+
42
+ .speed-brief {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: space-between;
46
+ gap: 1rem;
47
+ border-bottom: 1px solid var(--speed-line);
48
+ background: var(--speed-paper);
49
+ }
50
+
51
+ .speed-brief p,
52
+ .assumptions-row p {
53
+ max-width: 54rem;
54
+ margin: .45rem 0 0;
55
+ color: var(--speed-muted);
56
+ font-size: .82rem;
57
+ line-height: 1.55;
58
+ }
59
+
60
+ .brief-kicker {
61
+ color: var(--speed-teal);
62
+ font-size: .67rem;
63
+ font-weight: 900;
64
+ letter-spacing: .15em;
65
+ text-transform: uppercase;
66
+ }
67
+
68
+ .unit-switch,
69
+ .preset-track {
70
+ display: flex;
71
+ flex-wrap: wrap;
72
+ gap: .45rem;
73
+ }
74
+
75
+ button {
76
+ cursor: pointer;
77
+ font: inherit;
78
+ }
79
+
80
+ .unit-button,
81
+ .preset-button {
82
+ border: 1px solid var(--speed-line);
83
+ border-radius: .45rem;
84
+ background: transparent;
85
+ color: var(--speed-muted);
86
+ font-size: .74rem;
87
+ font-weight: 800;
88
+ padding: .62rem .75rem;
89
+ transition: transform .18s ease, background .18s ease, color .18s ease;
90
+ }
91
+
92
+ .unit-button:hover,
93
+ .preset-button:hover {
94
+ transform: translateY(-1px);
95
+ border-color: var(--speed-teal);
96
+ color: var(--speed-teal);
97
+ }
98
+
99
+ .unit-button.is-selected,
100
+ .preset-button.is-selected {
101
+ border-color: var(--speed-teal);
102
+ background: var(--speed-teal);
103
+ color: #fff;
104
+ }
105
+
106
+ .flight-deck {
107
+ display: grid;
108
+ grid-template-columns: minmax(15rem, .75fr) minmax(0, 1.75fr);
109
+ gap: clamp(1.25rem, 4vw, 3rem);
110
+ }
111
+
112
+ .control-rail {
113
+ padding-right: clamp(0rem, 2vw, 1.5rem);
114
+ border-right: 1px solid var(--speed-line);
115
+ }
116
+
117
+ h2,
118
+ h3 {
119
+ margin: .32rem 0 0;
120
+ line-height: 1.1;
121
+ }
122
+
123
+ h2 {
124
+ font-size: clamp(1.22rem, 2.6vw, 1.8rem);
125
+ }
126
+
127
+ h3 {
128
+ font-size: 1rem;
129
+ }
130
+
131
+ .preset-track {
132
+ margin: 1.25rem 0 1.6rem;
133
+ }
134
+
135
+ .input-list {
136
+ display: grid;
137
+ gap: .65rem;
138
+ }
139
+
140
+ .input-row {
141
+ position: relative;
142
+ display: grid;
143
+ gap: .3rem;
144
+ }
145
+
146
+ .input-row span {
147
+ color: var(--speed-muted);
148
+ font-size: .74rem;
149
+ font-weight: 800;
150
+ }
151
+
152
+ .input-row input {
153
+ width: 100%;
154
+ border: 1px solid var(--speed-line);
155
+ border-radius: .55rem;
156
+ background: var(--speed-paper);
157
+ color: var(--speed-ink);
158
+ font-size: 1.02rem;
159
+ font-weight: 800;
160
+ padding: .72rem 2.5rem .72rem .75rem;
161
+ }
162
+
163
+ .input-row small {
164
+ position: absolute;
165
+ right: .75rem;
166
+ bottom: .77rem;
167
+ color: var(--speed-muted);
168
+ font-size: .7rem;
169
+ font-weight: 900;
170
+ }
171
+
172
+ button:focus-visible,
173
+ input:focus-visible {
174
+ outline: 3px solid var(--speed-coral);
175
+ outline-offset: 2px;
176
+ }
177
+
178
+ .speed-orchestra {
179
+ min-width: 0;
180
+ }
181
+
182
+ .speed-orchestra-header,
183
+ .subheading {
184
+ display: flex;
185
+ align-items: flex-start;
186
+ justify-content: space-between;
187
+ gap: 1rem;
188
+ }
189
+
190
+ .diagnosis {
191
+ border: 1px solid var(--speed-teal);
192
+ border-radius: 999px;
193
+ color: var(--speed-teal);
194
+ font-size: .72rem;
195
+ font-weight: 900;
196
+ padding: .55rem .7rem;
197
+ text-align: center;
198
+ }
199
+
200
+ [data-diagnosis="overspeed"] .diagnosis,
201
+ [data-diagnosis="high-slip"] .diagnosis {
202
+ border-color: var(--speed-coral);
203
+ color: var(--speed-coral);
204
+ }
205
+
206
+ .speed-lane {
207
+ position: relative;
208
+ min-height: 19rem;
209
+ margin: 1.25rem 0;
210
+ overflow: hidden;
211
+ border-radius: 1rem;
212
+ background:
213
+ radial-gradient(circle at 78% 20%, rgba(199, 243, 109, .14), transparent 24%),
214
+ linear-gradient(155deg, #102a31 0%, var(--speed-slate) 58%, #14221e 100%);
215
+ }
216
+
217
+ .lane-grid {
218
+ position: absolute;
219
+ inset: 0;
220
+ opacity: .52;
221
+ background-image: linear-gradient(105deg, transparent 0 48%, rgba(199, 243, 109, .16) 49% 50%, transparent 51%), repeating-linear-gradient(90deg, transparent 0 2.9rem, rgba(255, 255, 255, .1) 3rem);
222
+ background-size: 100% 100%, 3rem 100%;
223
+ }
224
+
225
+ .lane-horizon {
226
+ position: absolute;
227
+ right: -10%;
228
+ bottom: 0;
229
+ left: -10%;
230
+ height: 5.5rem;
231
+ border-top: 1px solid rgba(199, 243, 109, .28);
232
+ background: linear-gradient(180deg, rgba(199, 243, 109, .1), transparent);
233
+ clip-path: polygon(0 45%, 100% 0, 100% 100%, 0 100%);
234
+ }
235
+
236
+ .lane-header {
237
+ position: absolute;
238
+ top: 1rem;
239
+ right: 1.15rem;
240
+ left: 1.15rem;
241
+ z-index: 1;
242
+ display: flex;
243
+ justify-content: space-between;
244
+ color: rgba(255, 255, 255, .7);
245
+ font-size: .65rem;
246
+ font-weight: 800;
247
+ letter-spacing: .04em;
248
+ text-transform: uppercase;
249
+ }
250
+
251
+ .lane-header > span:first-child {
252
+ color: var(--speed-lime);
253
+ letter-spacing: .14em;
254
+ }
255
+
256
+ .lane-header > span:last-child {
257
+ display: inline-flex;
258
+ align-items: center;
259
+ gap: .35rem;
260
+ color: rgba(255, 255, 255, .68);
261
+ letter-spacing: 0;
262
+ text-transform: none;
263
+ }
264
+
265
+ .lane-header b {
266
+ font-size: .58rem;
267
+ font-weight: 800;
268
+ }
269
+
270
+ .lane-header i {
271
+ display: inline-block;
272
+ width: 3rem;
273
+ height: 1px;
274
+ background: rgba(255, 255, 255, .36);
275
+ }
276
+
277
+ .speed-comparison {
278
+ position: absolute;
279
+ top: 5rem;
280
+ right: 1.15rem;
281
+ bottom: 1.25rem;
282
+ left: 1.15rem;
283
+ z-index: 1;
284
+ display: grid;
285
+ align-content: center;
286
+ gap: 1.2rem;
287
+ }
288
+
289
+ .comparison-scale {
290
+ display: grid;
291
+ grid-template-columns: repeat(3, 1fr);
292
+ color: rgba(255, 255, 255, .5);
293
+ font-size: .58rem;
294
+ font-weight: 800;
295
+ }
296
+
297
+ .comparison-scale span:nth-child(2) { text-align: center; }
298
+ .comparison-scale span:last-child { text-align: right; }
299
+
300
+ .comparison-row {
301
+ display: grid;
302
+ grid-template-columns: minmax(9.5rem, 1fr) minmax(10rem, 2.2fr);
303
+ align-items: center;
304
+ gap: 1rem;
305
+ }
306
+
307
+ .comparison-label {
308
+ display: grid;
309
+ gap: .35rem;
310
+ color: rgba(255, 255, 255, .7);
311
+ font-size: .65rem;
312
+ font-weight: 800;
313
+ line-height: 1.2;
314
+ }
315
+
316
+ .comparison-label strong {
317
+ color: var(--speed-lime);
318
+ font-size: clamp(1.1rem, 2.3vw, 1.55rem);
319
+ line-height: 1;
320
+ white-space: nowrap;
321
+ }
322
+
323
+ .comparison-row:nth-child(3) .comparison-label strong {
324
+ color: var(--speed-coral);
325
+ }
326
+
327
+ .comparison-track {
328
+ position: relative;
329
+ height: 1.15rem;
330
+ overflow: hidden;
331
+ border: 1px solid rgba(255, 255, 255, .22);
332
+ border-radius: 99rem;
333
+ background:
334
+ linear-gradient(90deg, transparent 0 49.7%, rgba(255, 255, 255, .16) 50%, transparent 50.3%),
335
+ rgba(255, 255, 255, .08);
336
+ }
337
+
338
+ .comparison-fill {
339
+ display: block;
340
+ height: 100%;
341
+ border-radius: inherit;
342
+ transition: width .3s ease;
343
+ }
344
+
345
+ .comparison-fill-estimated {
346
+ width: var(--speed-position, 58%);
347
+ background: linear-gradient(90deg, var(--speed-teal), var(--speed-lime));
348
+ box-shadow: 0 0 .9rem rgba(199, 243, 109, .42);
349
+ }
350
+
351
+ .comparison-fill-pitch {
352
+ width: var(--pitch-position, 70%);
353
+ background: repeating-linear-gradient(135deg, var(--speed-coral) 0 .45rem, #ffb095 .45rem .8rem);
354
+ opacity: .9;
355
+ }
356
+
357
+ .comparison-gap {
358
+ display: flex;
359
+ align-items: baseline;
360
+ justify-content: center;
361
+ gap: .55rem;
362
+ margin-top: .2rem;
363
+ padding-top: .85rem;
364
+ border-top: 1px solid rgba(255, 255, 255, .14);
365
+ color: rgba(255, 255, 255, .68);
366
+ font-size: .65rem;
367
+ font-weight: 800;
368
+ }
369
+
370
+ .comparison-gap strong {
371
+ color: var(--speed-coral);
372
+ font-size: 1.05rem;
373
+ }
374
+
375
+ .comparison-gap small {
376
+ color: rgba(255, 255, 255, .55);
377
+ font-size: .65rem;
378
+ }
379
+
380
+ .speed-readouts {
381
+ display: grid;
382
+ grid-template-columns: 1.4fr 1fr 1fr;
383
+ gap: 1px;
384
+ border: 1px solid var(--speed-line);
385
+ background: var(--speed-line);
386
+ }
387
+
388
+ .hero-readout,
389
+ .readout {
390
+ display: grid;
391
+ gap: .35rem;
392
+ min-width: 0;
393
+ padding: .9rem;
394
+ background: var(--speed-card);
395
+ }
396
+
397
+ .hero-readout strong {
398
+ color: var(--speed-teal);
399
+ font-size: clamp(1.25rem, 3vw, 1.9rem);
400
+ }
401
+
402
+ .readout strong {
403
+ font-size: clamp(1rem, 2vw, 1.35rem);
404
+ }
405
+
406
+ .hero-readout span,
407
+ .readout span,
408
+ .metric-strip span,
409
+ .sensitivity-point span,
410
+ .sensitivity-point small {
411
+ color: var(--speed-muted);
412
+ font-size: .68rem;
413
+ font-weight: 800;
414
+ }
415
+
416
+ .diagnosis-advice {
417
+ min-height: 2.7rem;
418
+ margin: 1rem 0;
419
+ color: var(--speed-muted);
420
+ font-size: .78rem;
421
+ line-height: 1.5;
422
+ }
423
+
424
+ .sensitivity {
425
+ border-top: 1px solid var(--speed-line);
426
+ padding-top: 1rem;
427
+ }
428
+
429
+ .sensitivity-row {
430
+ display: grid;
431
+ grid-template-columns: repeat(3, 1fr);
432
+ gap: .75rem;
433
+ margin-top: 1rem;
434
+ }
435
+
436
+ .sensitivity-point {
437
+ display: grid;
438
+ gap: .35rem;
439
+ min-width: 0;
440
+ padding: .75rem;
441
+ border-left: 3px solid var(--speed-line);
442
+ }
443
+
444
+ .sensitivity-point.is-current { border-left-color: var(--speed-coral); }
445
+ .sensitivity-point strong { font-size: 1rem; }
446
+ .sensitivity-point small { font-size: .68rem; }
447
+
448
+ .sensitivity-bar {
449
+ height: .32rem;
450
+ margin-top: .3rem;
451
+ overflow: hidden;
452
+ border-radius: 999px;
453
+ background: var(--speed-paper);
454
+ }
455
+
456
+ .sensitivity-bar i {
457
+ display: block;
458
+ width: var(--bar-width, 50%);
459
+ height: 100%;
460
+ border-radius: inherit;
461
+ background: var(--speed-teal);
462
+ transition: width .3s ease;
463
+ }
464
+
465
+ .metric-strip {
466
+ display: grid;
467
+ grid-template-columns: repeat(3, 1fr);
468
+ gap: 1rem;
469
+ margin-top: 1.25rem;
470
+ padding-top: 1rem;
471
+ border-top: 1px solid var(--speed-line);
472
+ }
473
+
474
+ .metric-strip div {
475
+ display: grid;
476
+ gap: .35rem;
477
+ }
478
+
479
+ .metric-strip strong {
480
+ font-size: .94rem;
481
+ }
482
+
483
+ .assumptions-row {
484
+ display: grid;
485
+ grid-template-columns: 1fr 1fr;
486
+ gap: 1.5rem;
487
+ border-top: 1px solid var(--speed-line);
488
+ background: var(--speed-paper);
489
+ }
490
+
491
+ .assumptions-row p {
492
+ margin: 0;
493
+ }
494
+
495
+ .assumptions-row strong {
496
+ color: var(--speed-ink);
497
+ }
498
+
499
+ @media (max-width: 780px) {
500
+ .speed-brief {
501
+ align-items: flex-start;
502
+ flex-direction: column;
503
+ }
504
+
505
+ .flight-deck {
506
+ grid-template-columns: 1fr;
507
+ }
508
+
509
+ .control-rail {
510
+ padding-right: 0;
511
+ border-right: 0;
512
+ border-bottom: 1px solid var(--speed-line);
513
+ padding-bottom: 1.5rem;
514
+ }
515
+ }
516
+
517
+ @media (max-width: 540px) {
518
+ .speed-readouts,
519
+ .assumptions-row { grid-template-columns: 1fr; }
520
+ .sensitivity-row { gap: .35rem; }
521
+ .sensitivity-point { padding: .45rem; }
522
+ .metric-strip { gap: .5rem; }
523
+ .metric-strip strong { font-size: .8rem; }
524
+ .speed-lane { min-height: 20rem; }
525
+ .lane-header {
526
+ right: .8rem;
527
+ left: .8rem;
528
+ font-size: .58rem;
529
+ }
530
+
531
+ .lane-header i {
532
+ width: 1.5rem;
533
+ }
534
+
535
+ .speed-comparison {
536
+ top: 4.4rem;
537
+ right: .8rem;
538
+ left: .8rem;
539
+ gap: 1rem;
540
+ }
541
+
542
+ .comparison-row {
543
+ grid-template-columns: minmax(7.5rem, 1fr) minmax(8rem, 1.35fr);
544
+ gap: .7rem;
545
+ }
546
+
547
+ .comparison-label {
548
+ font-size: .58rem;
549
+ }
550
+
551
+ .comparison-label strong {
552
+ font-size: 1rem;
553
+ }
554
+
555
+ .comparison-gap {
556
+ font-size: .58rem;
557
+ }
558
+ }
@@ -0,0 +1,20 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication, WithContext } from 'schema-dts';
2
+ import type { SEOSection } from '../../../types';
3
+ import type { FpvDroneSpeedLocaleContent } from '../entry';
4
+ import { BIBLIOGRAPHY_ITEMS } from '../bibliography';
5
+
6
+ const title = 'Geschwindigkeitsrechner für FPV Drohnen';
7
+ const description = 'Schätze die Vorwärtsgeschwindigkeit aus Motor KV, Akkuspannung, Propellersteigung, Wirkungsgrad und Flugmasse und sieh Schlupf und Empfindlichkeit getrennt.';
8
+ const ui = { presetsLabel: 'Mit einem Flugsetup starten', presetRacing: 'Racing 4S', presetFreestyle: 'Freestyle 6S', presetCruiser: 'Cruiser 7 Zoll', unitsLabel: 'Anzeigeeinheiten', metricUnit: 'Metrisch', imperialUnit: 'Imperial', inputsLabel: 'Flugrezept', motorKvLabel: 'Motor KV', batteryVoltageLabel: 'Akkuspannung', propellerPitchLabel: 'Propellersteigung', efficiencyLabel: 'Wirkungsgradschätzung', aircraftMassLabel: 'Flugmasse', calculateFromLabel: 'Schätzung aus fünf Eingaben', resultsLabel: 'Flugspur', estimatedSpeedLabel: 'Geschätzte Vorwärtsgeschwindigkeit', pitchSpeedLabel: 'Steiggeschwindigkeit ohne Schlupf', loadedRpmLabel: 'Motordrehzahl unter Last', noLoadRpmLabel: 'Drehzahl ohne Last', slipLabel: 'Durch Schlupf verlorene Geschwindigkeit', loadEffectLabel: 'Lastkorrektur', speedLaneLabel: 'Geschwindigkeitsspur des Propellers', sensitivityLabel: 'Nur eine Änderung auf einmal', lowerPitchLabel: 'Kleinere Steigung', selectedPitchLabel: 'Gewählte Steigung', higherPitchLabel: 'Größere Steigung', diagnosisLabel: 'Einordnung', diagnosisPlanning: 'Planungsschätzung', diagnosisHighSlip: 'Hoher Schlupf', diagnosisHeavyLoad: 'Hohe Last', diagnosisOverspeed: 'Drehzahl prüfen', diagnosisPlanningAdvice: 'Nutze dies als erste Größenordnung und vergleiche sie danach mit Herstellerdaten oder einem Prüfstand.', diagnosisHighSlipAdvice: 'Der gewählte Wirkungsgrad lässt eine große Lücke zwischen geometrischer Steiggeschwindigkeit und Vorwärtsflug. Prüfe Last, Spannungseinbruch und Motortemperatur.', diagnosisHeavyLoadAdvice: 'Die Flugmasse macht die Lastkorrektur wichtiger. Behandle die Geschwindigkeit als groben Planungsbereich und prüfe den kompletten Antrieb.', diagnosisOverspeedAdvice: 'Die Leerlaufdrehzahl ist für dieses Setup ungewöhnlich hoch. Prüfe Motor, Akku und Propellergrenzen vor dem Einschalten.', assumptionsLabel: 'Modellgrenze.', assumptionsText: 'Die Steiggeschwindigkeit ist geometrisch: Steigung mal belastete Drehzahl. Der Wirkungsgrad ist ein eingegebener Näherungswert für Schlupf; die Masse erzeugt eine kleine, sichtbare Lastkorrektur. Propellerdurchmesser, Schubkurve und Wind fehlen.', safetyText: 'Keine Fluggarantie. Halte Abstand zum Propeller und prüfe Strom, Temperatur, Vibrationen und Geschwindigkeit kontrolliert.', speedAxisStart: '0 km/h', speedAxisEnd: '300 km/h', noLoadCaption: 'ohne Last', loadedCaption: 'belastet', slipCaption: 'Schlupf', massUnit: 'g', speedUnit: 'km/h' } satisfies Record<string, string>;
9
+ const faq = [
10
+ { question: 'Wie wird die Steiggeschwindigkeit einer FPV Drohne berechnet?', answer: 'Die Propellersteigung wird mit der belasteten Drehzahl multipliziert und von Zoll pro Umdrehung in Kilometer pro Stunde umgerechnet. Das ist eine geometrische Geschwindigkeit ohne Schlupf und keine Zusage für die Fluggeschwindigkeit.' },
11
+ { question: 'Was bedeutet Wirkungsgrad in dieser Schätzung?', answer: 'Der Wirkungsgrad ist ein Planungswert für die Lücke zwischen geometrischer Steiggeschwindigkeit und geschätztem Vortrieb. Er ersetzt keine Propellerkennlinie und keinen Flugtest.' },
12
+ { question: 'Warum beeinflusst die Flugmasse das Ergebnis?', answer: 'Ein schweres Fluggerät belastet den Antrieb meist stärker. Das Modell verwendet deshalb eine kleine begrenzte Drehzahlkorrektur. Ohne Durchmesser, Schubdaten und Motordrehmoment bleibt sie eine transparente Näherung.' },
13
+ { question: 'Bestätigt der Rechner, dass die Drohne sicher fliegt?', answer: 'Nein. Motor, ESC, Akku, Propeller, Rahmen und Flugsteuerung müssen anhand ihrer Herstellergrenzen und eines gesicherten Prüfstands geprüft werden.' },
14
+ { question: 'Warum ist die echte Geschwindigkeit kleiner als die Steigungsgeschwindigkeit?', answer: 'Die geometrische Geschwindigkeit nimmt an, dass der Propeller seine Nennsteigung pro Umdrehung vorrückt. Schlupf, Blattform, Spannungseinbruch, Last und Einbauverluste verringern den nutzbaren Vortrieb.' },
15
+ ];
16
+ const howTo = [{ name: 'Antrieb eingeben', text: 'Gib Motor KV und die erwartete Akkuspannung unter Last ein, nicht nur den Nennwert des Akkus.' }, { name: 'Propeller und Fluggerät beschreiben', text: 'Gib Steigung, einen vorsichtigen Wirkungsgrad und die flugfertige Masse ein. Ein Preset hilft beim Start.' }, { name: 'Spur lesen und prüfen', text: 'Vergleiche Schätzung, Steiggeschwindigkeit, Schlupf und Steigungsempfindlichkeit. Verwende das Ergebnis als Prüfplan.' }];
17
+ const seo: SEOSection[] = [{ type: 'title', text: 'FPV Drohnengeschwindigkeit aus der Propellersteigung schätzen', level: 2 }, { type: 'paragraph', html: 'Eine Geschwindigkeitsschätzung für FPV Drohnen beginnt mit der Strecke, die ein Propeller mit fester Steigung pro Umdrehung zurücklegen würde. Motor KV und Akkuspannung liefern eine erste Drehzahl; Wirkungsgrad und Flugmasse setzen die Zahl in einen vorsichtigen Planungsrahmen. Der Rechner zeigt geometrische Steiggeschwindigkeit und geschätzten Vortrieb gemeinsam, damit Annahmen nicht mit Messwerten verwechselt werden.' }, { type: 'title', text: 'Die Geschwindigkeitsspur lesen', level: 2 }, { type: 'table', headers: ['Signal', 'Bedeutung'], rows: [['Steigung ohne Schlupf', 'Steigung mal belastete Drehzahl; jede Umdrehung bewegt den Propeller um seine Nennsteigung.'], ['Geschätzter Vortrieb', 'Der Wert ohne Schlupf, multipliziert mit dem eingegebenen Wirkungsgrad.'], ['Verlorene Geschwindigkeit', 'Prozentuale Lücke zwischen geometrischer Steigung und geschätztem Vortrieb.'], ['Steigungsempfindlichkeit', 'Ergebnis bei 0,5 Zoll weniger oder mehr Steigung im gleichen Setup.']] }, { type: 'title', text: 'Die Schätzung sinnvoll verwenden', level: 2 }, { type: 'list', items: ['Verwende die erwartete Spannung unter Last.', 'Beginne mit bekannten Motor Propeller Daten oder Herstellerwerten.', 'Nutze hohen Schlupf als Hinweis, Last, Spannungseinbruch und Propellerwahl zu prüfen.', 'Kontrolliere Strom, Temperatur, Vibrationen und echte Geschwindigkeit vor dem Flug.'] }, { type: 'paragraph', html: 'Propellerleistung verändert sich mit Fortschrittsgrad, Blattgeometrie, Luftdichte, Zufluss und Betriebswinkel. Forschungsmethoden für UAV Propeller verwenden gemessene oder identifizierte Aerodynamikdaten. Dieser Rechner bleibt bewusst einfacher und ersetzt weder Kennlinie noch Schubprüfstand.' }, { type: 'tip', title: 'Die Lücke als Entscheidungshilfe nutzen', html: 'Wenn ein Setup nur wegen einer hohen Zahl ohne Schlupf schnell wirkt, prüfe die Differenz zur wirkungsgradbereinigten Schätzung. Eine kleinere Steigung kann die Last verringern, braucht aber weiterhin eine echte Antriebsprüfung.' }];
18
+ const schemas: FpvDroneSpeedLocaleContent['schemas'] = [{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) } as WithContext<FAQPage>, { '@context': 'https://schema.org', '@type': 'HowTo', name: title, description, step: howTo.map((step, index) => ({ '@type': 'HowToStep', position: index + 1, name: step.name, text: step.text })) } as WithContext<HowTo>, { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'UtilityApplication', operatingSystem: 'All', offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' } } as WithContext<SoftwareApplication>];
19
+ export const content: FpvDroneSpeedLocaleContent = { slug: 'fpv-drohnen-geschwindigkeitsrechner', title, description, ui, seo, faq, bibliography: BIBLIOGRAPHY_ITEMS, howTo, schemas };
20
+ seo.push({ type: 'title', text: 'Messung bleibt der letzte Schritt', level: 2 });