@oicl/openbridge-webcomponents 0.0.15-dev-20240916185711 → 0.0.15-dev-20240923190011

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. package/.storybook/main.ts +8 -0
  2. package/.storybook/preview.ts +2 -0
  3. package/__snapshots__/building-blocks-watch--advice.png +0 -0
  4. package/__snapshots__/building-blocks-watch-flat--primary.png +0 -0
  5. package/__snapshots__/navigation-instruments-azimuth-thruster--in-command.png +0 -0
  6. package/__snapshots__/navigation-instruments-azimuth-thruster-labeled--large.png +0 -0
  7. package/__snapshots__/navigation-instruments-azimuth-thruster-labeled--medium.png +0 -0
  8. package/__snapshots__/navigation-instruments-azimuth-thruster-labeled--no-command.png +0 -0
  9. package/__snapshots__/navigation-instruments-compass--primary.png +0 -0
  10. package/__snapshots__/navigation-instruments-compass-flat--primary.png +0 -0
  11. package/__snapshots__/navigation-instruments-compass-flat--with-fov-indicator.png +0 -0
  12. package/custom-elements.json +968 -7
  13. package/dist/navigation-instruments/compass/arrow.d.ts +7 -0
  14. package/dist/navigation-instruments/compass/arrow.d.ts.map +1 -0
  15. package/dist/navigation-instruments/compass/arrow.js +59 -0
  16. package/dist/navigation-instruments/compass/arrow.js.map +1 -0
  17. package/dist/navigation-instruments/compass/compass.d.ts +23 -0
  18. package/dist/navigation-instruments/compass/compass.d.ts.map +1 -0
  19. package/dist/navigation-instruments/compass/compass.js +139 -0
  20. package/dist/navigation-instruments/compass/compass.js.map +1 -0
  21. package/dist/navigation-instruments/compass/radial-tickmark.d.ts +4 -0
  22. package/dist/navigation-instruments/compass/radial-tickmark.d.ts.map +1 -0
  23. package/dist/navigation-instruments/compass/radial-tickmark.js +69 -0
  24. package/dist/navigation-instruments/compass/radial-tickmark.js.map +1 -0
  25. package/dist/navigation-instruments/compass-flat/compass-flat.css.js +29 -0
  26. package/dist/navigation-instruments/compass-flat/compass-flat.css.js.map +1 -0
  27. package/dist/navigation-instruments/compass-flat/compass-flat.d.ts +45 -0
  28. package/dist/navigation-instruments/compass-flat/compass-flat.d.ts.map +1 -0
  29. package/dist/navigation-instruments/compass-flat/compass-flat.js +223 -0
  30. package/dist/navigation-instruments/compass-flat/compass-flat.js.map +1 -0
  31. package/dist/navigation-instruments/thruster/advice.d.ts.map +1 -1
  32. package/dist/navigation-instruments/thruster/advice.js +9 -5
  33. package/dist/navigation-instruments/thruster/advice.js.map +1 -1
  34. package/dist/navigation-instruments/watch/advice.js +1 -1
  35. package/dist/navigation-instruments/watch/advice.js.map +1 -1
  36. package/dist/navigation-instruments/watch/label.d.ts +3 -0
  37. package/dist/navigation-instruments/watch/label.d.ts.map +1 -0
  38. package/dist/navigation-instruments/watch/label.js +68 -0
  39. package/dist/navigation-instruments/watch/label.js.map +1 -0
  40. package/dist/navigation-instruments/watch/watch.css.js +15 -14
  41. package/dist/navigation-instruments/watch/watch.css.js.map +1 -1
  42. package/dist/navigation-instruments/watch/watch.d.ts +3 -0
  43. package/dist/navigation-instruments/watch/watch.d.ts.map +1 -1
  44. package/dist/navigation-instruments/watch/watch.js +34 -1
  45. package/dist/navigation-instruments/watch/watch.js.map +1 -1
  46. package/dist/navigation-instruments/watch-flat/tickmark-flat.d.ts +20 -0
  47. package/dist/navigation-instruments/watch-flat/tickmark-flat.d.ts.map +1 -0
  48. package/dist/navigation-instruments/watch-flat/tickmark-flat.js +53 -0
  49. package/dist/navigation-instruments/watch-flat/tickmark-flat.js.map +1 -0
  50. package/dist/navigation-instruments/watch-flat/watch-flat.css.js +32 -0
  51. package/dist/navigation-instruments/watch-flat/watch-flat.css.js.map +1 -0
  52. package/dist/navigation-instruments/watch-flat/watch-flat.d.ts +29 -0
  53. package/dist/navigation-instruments/watch-flat/watch-flat.d.ts.map +1 -0
  54. package/dist/navigation-instruments/watch-flat/watch-flat.js +184 -0
  55. package/dist/navigation-instruments/watch-flat/watch-flat.js.map +1 -0
  56. package/dist/svghelpers/rectangular.d.ts +1 -0
  57. package/dist/svghelpers/rectangular.d.ts.map +1 -1
  58. package/dist/svghelpers/rectangular.js +3 -2
  59. package/dist/svghelpers/rectangular.js.map +1 -1
  60. package/package.json +16 -11
  61. package/src/navigation-instruments/compass/arrow.ts +61 -0
  62. package/src/navigation-instruments/compass/compass.stories.ts +37 -0
  63. package/src/navigation-instruments/compass/compass.ts +132 -0
  64. package/src/navigation-instruments/compass/radial-tickmark.ts +77 -0
  65. package/src/navigation-instruments/compass-flat/compass-flat.css +23 -0
  66. package/src/navigation-instruments/compass-flat/compass-flat.stories.ts +35 -0
  67. package/src/navigation-instruments/compass-flat/compass-flat.ts +221 -0
  68. package/src/navigation-instruments/thruster/advice.ts +9 -5
  69. package/src/navigation-instruments/watch/advice.ts +1 -1
  70. package/src/navigation-instruments/watch/label.ts +69 -0
  71. package/src/navigation-instruments/watch/watch.css +7 -7
  72. package/src/navigation-instruments/watch/watch.ts +30 -1
  73. package/src/navigation-instruments/watch-flat/tickmark-flat.ts +62 -0
  74. package/src/navigation-instruments/watch-flat/watch-flat.css +19 -0
  75. package/src/navigation-instruments/watch-flat/watch-flat.stories.ts +17 -0
  76. package/src/navigation-instruments/watch-flat/watch-flat.ts +148 -0
  77. package/src/svghelpers/rectangular.ts +6 -3
@@ -0,0 +1,221 @@
1
+ import {LitElement, svg, SVGTemplateResult, unsafeCSS} from 'lit';
2
+ import componentStyle from './compass-flat.css?inline';
3
+ import {customElement, property} from 'lit/decorators.js';
4
+ import {Tickmark, TickmarkType} from '../watch-flat/tickmark-flat';
5
+ import '../watch-flat/watch-flat';
6
+
7
+ export enum LabelPosition {
8
+ top = -45,
9
+ bottom = 50,
10
+ }
11
+
12
+ export enum LabelStyle {
13
+ regular = 'var(--instrument-tick-mark-secondary-color)',
14
+ }
15
+
16
+ export interface Label {
17
+ x: number;
18
+ y: LabelPosition;
19
+ text: string;
20
+ }
21
+
22
+ @customElement('obc-compass-flat')
23
+ export class ObcCompassFlat extends LitElement {
24
+ @property({type: Boolean}) noPadding: boolean = true;
25
+ @property({type: Boolean}) FOVIndicator: boolean = false;
26
+ @property({type: Number}) padding: number = 16;
27
+ @property({type: Number}) heading = 0;
28
+ @property({type: Number}) courseOverGround = 0;
29
+ @property({type: Number}) tickInterval = 5;
30
+ @property({type: Number}) FOV = 45;
31
+ @property({type: Number}) minFOV = 45;
32
+ @property({type: Number}) maxFOV = 180;
33
+ labels: Label[] = [];
34
+
35
+ @property({type: Number}) containerWidth = 0;
36
+
37
+ private resizeObserver: ResizeObserver = new ResizeObserver((entries) => {
38
+ for (const entry of entries) {
39
+ this.containerWidth = entry.contentRect.width;
40
+ this.updateLabels();
41
+ }
42
+ });
43
+
44
+ override connectedCallback() {
45
+ super.connectedCallback();
46
+ this.resizeObserver.observe(this);
47
+ }
48
+
49
+ override disconnectedCallback() {
50
+ super.disconnectedCallback();
51
+ this.resizeObserver.unobserve(this);
52
+ }
53
+
54
+ private updateLabels() {
55
+ if (this.containerWidth < 192) {
56
+ this.labels = [];
57
+ } else if (this.containerWidth <= 300) {
58
+ this.labels = [
59
+ {x: -180, y: LabelPosition.top, text: 'S'},
60
+ {x: -90, y: LabelPosition.top, text: 'W'},
61
+ {x: 0, y: LabelPosition.top, text: 'N'},
62
+ {x: 90, y: LabelPosition.top, text: 'E'},
63
+ {x: 180, y: LabelPosition.top, text: 'S'},
64
+ {x: 270, y: LabelPosition.top, text: 'W'},
65
+ {x: 360, y: LabelPosition.top, text: 'N'},
66
+ {x: 450, y: LabelPosition.top, text: 'E'},
67
+ {x: 540, y: LabelPosition.top, text: 'S'},
68
+ ];
69
+ } else {
70
+ this.labels = [
71
+ {x: -180, y: LabelPosition.top, text: 'S'},
72
+ {x: -135, y: LabelPosition.top, text: 'SW'},
73
+ {x: -90, y: LabelPosition.top, text: 'W'},
74
+ {x: -45, y: LabelPosition.top, text: 'NW'},
75
+ {x: 0, y: LabelPosition.top, text: 'N'},
76
+ {x: 45, y: LabelPosition.top, text: 'NE'},
77
+ {x: 90, y: LabelPosition.top, text: 'E'},
78
+ {x: 135, y: LabelPosition.top, text: 'SE'},
79
+ {x: 180, y: LabelPosition.top, text: 'S'},
80
+ {x: 225, y: LabelPosition.top, text: 'SW'},
81
+ {x: 270, y: LabelPosition.top, text: 'W'},
82
+ {x: 315, y: LabelPosition.top, text: 'NW'},
83
+ {x: 360, y: LabelPosition.top, text: 'N'},
84
+ {x: 405, y: LabelPosition.top, text: 'NE'},
85
+ {x: 450, y: LabelPosition.top, text: 'E'},
86
+ {x: 495, y: LabelPosition.top, text: 'SE'},
87
+ {x: 540, y: LabelPosition.top, text: 'S'},
88
+ ];
89
+ }
90
+ }
91
+
92
+ private generateIntervalTickmarks(scale: number): Tickmark[] {
93
+ const tickmarks: Tickmark[] = [];
94
+ let cardinalInterval = 90;
95
+
96
+ if (this.containerWidth > 300) {
97
+ cardinalInterval = 45;
98
+ } else if (this.containerWidth < 192) {
99
+ cardinalInterval = 0;
100
+ }
101
+
102
+ for (
103
+ let angle = -180;
104
+ angle < this.maxFOV * 3;
105
+ angle += this.tickInterval
106
+ ) {
107
+ if (cardinalInterval !== 0 && angle % cardinalInterval === 0) {
108
+ continue;
109
+ }
110
+ tickmarks.push({angle: angle * scale, type: TickmarkType.secondary});
111
+ }
112
+
113
+ return tickmarks;
114
+ }
115
+
116
+ private generateCardinalTickmarks(scale: number): Tickmark[] {
117
+ const tickmarks: Tickmark[] = [];
118
+
119
+ for (const label of this.labels) {
120
+ tickmarks.push({angle: label.x * scale, type: TickmarkType.main});
121
+ }
122
+
123
+ return tickmarks;
124
+ }
125
+
126
+ private generateTickmarks(scale: number): Tickmark[] {
127
+ return [
128
+ ...this.generateCardinalTickmarks(scale),
129
+ ...this.generateIntervalTickmarks(scale),
130
+ ];
131
+ }
132
+
133
+ private renderFOVIndicator(): SVGTemplateResult[] {
134
+ const indicators: SVGTemplateResult[] = [];
135
+
136
+ const maxAdjustment = 10;
137
+ const minContainerWidth = 300;
138
+ const maxContainerWidth = 512;
139
+
140
+ let yAdjustment = 0;
141
+ if (this.containerWidth < maxContainerWidth) {
142
+ const widthRange = maxContainerWidth - minContainerWidth;
143
+ const scaleFactor =
144
+ (maxContainerWidth - this.containerWidth) / widthRange;
145
+ yAdjustment = scaleFactor * maxAdjustment;
146
+ }
147
+
148
+ const y = LabelPosition.bottom + yAdjustment;
149
+
150
+ indicators.push(svg`
151
+ <text x="-175" y=${y} class="label left" fill=${LabelStyle.regular}>
152
+ ${-this.FOV}\u00B0
153
+ </text>`);
154
+
155
+ indicators.push(svg`
156
+ <text x="0" y=${y} class="label" fill=${LabelStyle.regular}>
157
+ ${this.heading}\u00B0
158
+ </text>`);
159
+
160
+ indicators.push(svg`
161
+ <text x="175" y=${y} class="label right" fill=${LabelStyle.regular}>
162
+ ${this.FOV}\u00B0
163
+ </text>`);
164
+
165
+ return indicators;
166
+ }
167
+
168
+ private get HDGSvg(): SVGTemplateResult {
169
+ return svg`<g transform="translate(-24, -74)">
170
+ <path d="M36.7011 44.1445L36.6898 44.1379L36.6781 44.1318L24.2301 37.6823L24.0001 37.5631L23.7701 37.6823L11.3221 44.1318L11.3104 44.1379L11.2991 44.1445C9.25497 45.3438 6.78661 43.308 7.68828 41.0919L22.6036 4.43285C23.1096 3.18905 24.8906 3.18905 25.3967 4.43284L40.3119 41.0919C41.2136 43.308 38.7452 45.3438 36.7011 44.1445Z" fill="var(--instrument-enhanced-secondary-color)" stroke="var(--border-silhouette-color)"/>
171
+ </g>`;
172
+ }
173
+
174
+ private COGSvg(translation: number): SVGTemplateResult {
175
+ return svg`
176
+ <g transform="translate(${-24 + translation}, -74)">
177
+ <path d="M31.9025 36.0262L33.1068 36.6502L32.5956 35.3938L24.4632 15.406L24.0001 14.2677L23.537 15.406L15.4046 35.3938L14.8935 36.6502L16.0978 36.0262L24.0001 31.9319L31.9025 36.0262ZM36.7011 44.1445L36.6898 44.1379L36.6781 44.1318L24.2301 37.6823L24.0001 37.5631L23.7701 37.6823L11.3221 44.1318L11.3104 44.1379L11.2991 44.1445C9.25497 45.3438 6.78661 43.308 7.68828 41.0919L22.6036 4.43285C23.1096 3.18905 24.8906 3.18905 25.3967 4.43284L40.3119 41.0919C41.2136 43.308 38.7452 45.3438 36.7011 44.1445Z" fill="var(--instrument-enhanced-secondary-color)" stroke="var(--border-silhouette-color)"/>
178
+ </g>
179
+ `;
180
+ }
181
+
182
+ override render() {
183
+ let angleDiff = this.courseOverGround - this.heading;
184
+
185
+ if (angleDiff > this.maxFOV) {
186
+ angleDiff -= 360;
187
+ } else if (angleDiff < -this.maxFOV) {
188
+ angleDiff += 360;
189
+ }
190
+
191
+ this.FOV = Math.max(this.minFOV, Math.abs(angleDiff));
192
+
193
+ const baseOffset = 5;
194
+ const translationScale = (baseOffset * 35) / this.FOV;
195
+
196
+ const translation = angleDiff * translationScale;
197
+
198
+ const tickmarks = this.generateTickmarks(translationScale);
199
+ this.labels.map((l) => {
200
+ l.x = l.x * translationScale;
201
+ });
202
+
203
+ const viewBox = this.noPadding ? '-192 -128 384 128' : '-200 -144 400 144';
204
+
205
+ return svg`
206
+ <div class="container">
207
+ <obc-watch-flat .noPadding=${this.noPadding} .FOVIndicator=${this.FOVIndicator ? this.renderFOVIndicator() : []} .labels=${this.labels} .rotation=${this.heading} .tickmarks=${tickmarks} .tickmarkSpacing=${translationScale}></obc-watch-flat>
208
+ <svg viewBox=${viewBox} xmlns="http://www.w3.org/2000/svg">
209
+ ${this.HDGSvg}${this.COGSvg(translation)}
210
+ </div>
211
+ `;
212
+ }
213
+
214
+ static override styles = unsafeCSS(componentStyle);
215
+ }
216
+
217
+ declare global {
218
+ interface HTMLElementTagNameMap {
219
+ 'obc-compass-flat': ObcCompassFlat;
220
+ }
221
+ }
@@ -84,20 +84,24 @@ export function renderAdvice(
84
84
  ${singleSidedTickmark(height, advice.max, tickmarkStyle)}
85
85
  `;
86
86
  } else {
87
- let mainColor;
87
+ let strokeColor;
88
88
  let tickmarkStyle;
89
+ let fillColor: string;
89
90
  if (advice.state === AdviceState.hinted) {
90
- mainColor = 'var(--instrument-frame-tertiary-color)';
91
+ strokeColor = 'var(--instrument-frame-tertiary-color)';
92
+ fillColor = 'none';
91
93
  tickmarkStyle = TickmarkStyle.hinted;
92
94
  } else if (advice.state === AdviceState.regular) {
93
- mainColor = 'var(--instrument-regular-secondary-color)';
95
+ strokeColor = 'var(--instrument-regular-secondary-color)';
96
+ fillColor = 'none';
94
97
  tickmarkStyle = TickmarkStyle.regular;
95
98
  } else {
96
- mainColor = 'var(--instrument-enhanced-secondary-color)';
99
+ strokeColor = 'var(--instrument-enhanced-secondary-color)';
100
+ fillColor = strokeColor;
97
101
  tickmarkStyle = TickmarkStyle.regular;
98
102
  }
99
103
  return svg`
100
- ${adviceMask(height, advice.min, advice.max, mainColor, mainColor)}
104
+ ${adviceMask(height, advice.min, advice.max, fillColor, strokeColor)}
101
105
  ${singleSidedTickmark(height, advice.min, tickmarkStyle)}
102
106
  ${singleSidedTickmark(height, advice.max, tickmarkStyle)}
103
107
  `;
@@ -112,7 +112,7 @@ export function renderAdvice(advice: AngleAdviceRaw): SVGTemplateResult {
112
112
  tickmarkStyle = TickmarkStyle.regular;
113
113
  }
114
114
  return svg`
115
- ${adviceMask(advice.minAngle, advice.maxAngle, mainColor, mainColor)}
115
+ ${adviceMask(advice.minAngle, advice.maxAngle, advice.state === AdviceState.triggered ? mainColor : 'none', mainColor)}
116
116
  ${tickmark(advice.minAngle, TickmarkType.primary, tickmarkStyle, 1)}
117
117
  ${tickmark(advice.maxAngle, TickmarkType.primary, tickmarkStyle, 1)}
118
118
  `;
@@ -0,0 +1,69 @@
1
+ import {svg, SVGTemplateResult} from 'lit-html';
2
+
3
+ export function renderLabels(scale: number): SVGTemplateResult {
4
+ const labelWidth = 32;
5
+ const gap = 8;
6
+ const radius: number = 368 / 2;
7
+ const labels = [
8
+ {
9
+ label: 'E',
10
+ x: radius + gap / scale + labelWidth / 2,
11
+ y: 0,
12
+ class: 'label',
13
+ },
14
+ {
15
+ label: 'S',
16
+ x: 0,
17
+ y: radius + gap / scale + labelWidth / 2,
18
+ class: 'label',
19
+ },
20
+ {
21
+ label: 'W',
22
+ x: -(radius + gap / scale + labelWidth / 2),
23
+ y: 0,
24
+ class: 'label',
25
+ },
26
+ ];
27
+
28
+ let arrow = svg`<defs>
29
+ <mask id="circleMask">
30
+ <rect x="-${radius}" y="-${radius}" width="${radius * 2}" height="${radius * 2}" fill="black"/>
31
+ <circle cx="0" cy="0" r="${radius}" fill="white"/>
32
+ </mask>
33
+ </defs>
34
+ <g mask="url(#circleMask)" transform="translate(0, ${-(radius + 45 / scale)}) scale(${1 / scale}, ${1 / scale})">
35
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M-32.04 42.7192 0 0 32.039 42.7192C21.627 40.9314 10.922 40 0 40-10.922 40-21.627 40.9314-32.04 42.7192Z"
36
+ fill="var(--instrument-tick-mark-secondary-color)"/>
37
+ <path d="M5.003 29H2.091L-3.013 20.264H-3.077C-3.066 20.52-3.056 20.7813-3.045 21.048-3.034 21.3147-3.024 21.5867-3.013 21.864-2.992 22.1307-2.976 22.4027-2.965 22.68-2.954 22.9573-2.944 23.2347-2.933 23.512V29H-4.997V17.576H-2.101L2.987 26.232H3.035C3.024 25.9867 3.014 25.736 3.003 25.48 2.992 25.2133 2.982 24.952 2.971 24.696 2.971 24.4293 2.966 24.1627 2.955 23.896 2.944 23.6293 2.934 23.3627 2.923 23.096V17.576H5.003V29Z" fill="var(--element-active-inverted-color)"/>
38
+ </g>`;
39
+
40
+ if (scale < 0.58) {
41
+ arrow = svg`
42
+ <g mask="url(#circleMask)" transform="translate(0, ${-radius})">
43
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M-17.8457 24.984 0 0 17.8458 24.984C11.9868 24.3338 6.0324 24 0 24-6.0323 24-11.9867 24.3338-17.8457 24.984Z" fill="var(--instrument-frame-tertiary-color)"/>
44
+ </g>`;
45
+
46
+ labels.push({
47
+ label: 'N',
48
+ x: 0,
49
+ y: -(radius + gap / scale + labelWidth / 2),
50
+ class: 'label',
51
+ });
52
+ }
53
+
54
+ return svg`
55
+ ${arrow}
56
+
57
+ ${labels.map(
58
+ (l) => svg`
59
+ <text
60
+ x="${l.x}"
61
+ y="${l.y}"
62
+ class="${l.class}"
63
+ >
64
+ ${l.label}
65
+ </text>
66
+ `
67
+ )}
68
+ `;
69
+ }
@@ -1,11 +1,11 @@
1
1
  * {
2
- box-sizing: border-box;
2
+ box-sizing: border-box;
3
3
  }
4
4
 
5
5
  .label {
6
- @mixin font-body;
7
- font-size: calc(16px / var(--scale));
8
- fill: var(--element-neutral-color);
9
- alignment-baseline: middle;
10
- text-anchor: middle;
11
- }
6
+ @mixin font-body;
7
+ font-size: calc(16px / var(--scale));
8
+ fill: var(--element-neutral-color);
9
+ alignment-baseline: middle;
10
+ text-anchor: middle;
11
+ }
@@ -14,6 +14,7 @@ import compentStyle from './watch.css?inline';
14
14
  import {ResizeController} from '@lit-labs/observers/resize-controller.js';
15
15
  import {AngleAdviceRaw, renderAdvice} from './advice';
16
16
  import {Tickmark, TickmarkStyle, tickmark} from './tickmark';
17
+ import {renderLabels} from './label';
17
18
 
18
19
  @customElement('obc-watch')
19
20
  export class ObcWatch extends LitElement {
@@ -27,6 +28,8 @@ export class ObcWatch extends LitElement {
27
28
  @property({type: Boolean}) roundInsideCut = false;
28
29
  @property({type: Array, attribute: false}) tickmarks: Tickmark[] = [];
29
30
  @property({type: Array, attribute: false}) advices: AngleAdviceRaw[] = [];
31
+ @property({type: Boolean}) crosshairEnabled: boolean = false;
32
+ @property({type: Boolean}) labelFrameEnabled: boolean = false;
30
33
 
31
34
  // @ts-expect-error TS6133: The controller unsures that the render
32
35
  // function is called on resize of the element
@@ -91,6 +94,29 @@ export class ObcWatch extends LitElement {
91
94
  }
92
95
  }
93
96
 
97
+ private renderCrosshair(radius: number): SVGTemplateResult {
98
+ return svg`
99
+ <line
100
+ x1="-${radius}"
101
+ y1="0"
102
+ x2="${radius}"
103
+ y2="0"
104
+ stroke="var(--instrument-frame-tertiary-color)"
105
+ stroke-width="1"
106
+ vector-effect="non-scaling-stroke"
107
+ />
108
+ <line
109
+ x1="0"
110
+ y1="-${radius}"
111
+ x2="0"
112
+ y2="${radius}"
113
+ stroke="var(--instrument-frame-tertiary-color)"
114
+ stroke-width="1"
115
+ vector-effect="non-scaling-stroke"
116
+ />
117
+ `;
118
+ }
119
+
94
120
  override render() {
95
121
  const width = (176 + this.padding) * 2;
96
122
  const viewBox = `-${width / 2} -${width / 2} ${width} ${width}`;
@@ -102,6 +128,8 @@ export class ObcWatch extends LitElement {
102
128
  const advices = this.advices
103
129
  ? this.advices.map((a) => renderAdvice(a))
104
130
  : nothing;
131
+ const labels = this.labelFrameEnabled ? renderLabels(scale) : nothing;
132
+
105
133
  return html`
106
134
  <svg
107
135
  width="100%"
@@ -109,7 +137,8 @@ export class ObcWatch extends LitElement {
109
137
  viewBox=${viewBox}
110
138
  style="--scale: ${scale}"
111
139
  >
112
- ${this.watchCircle()} ${tickmarks} ${advices} ${angleSetpoint}
140
+ ${this.watchCircle()} ${tickmarks} ${advices} ${angleSetpoint} ${labels}
141
+ ${this.crosshairEnabled ? this.renderCrosshair(320 / 2) : nothing}
113
142
  </svg>
114
143
  `;
115
144
  }
@@ -0,0 +1,62 @@
1
+ import {SVGTemplateResult, svg} from 'lit';
2
+
3
+ export interface Tickmark {
4
+ angle: number;
5
+ type: TickmarkType;
6
+ text?: string;
7
+ }
8
+
9
+ export enum TickmarkType {
10
+ main = 'main',
11
+ primary = 'primary',
12
+ secondary = 'secondary',
13
+ tertiary = 'tertiary',
14
+ }
15
+
16
+ export enum TickmarkStyle {
17
+ hinted = 'hinted',
18
+ regular = 'regular',
19
+ enhanced = 'enhanced',
20
+ }
21
+
22
+ export function tickmarkColor(style: TickmarkStyle): string {
23
+ if (style === TickmarkStyle.hinted) {
24
+ return 'var(--instrument-frame-tertiary-color)';
25
+ } else if (style === TickmarkStyle.regular) {
26
+ return 'var(--instrument-tick-mark-secondary-color)';
27
+ } else {
28
+ return 'var(--instrument-tick-mark-primary-color)';
29
+ }
30
+ }
31
+
32
+ export function tickmark(
33
+ angle: number,
34
+ tickmarkSize: TickmarkType,
35
+ style: TickmarkStyle,
36
+ text?: string
37
+ ): SVGTemplateResult | SVGTemplateResult[] {
38
+ const textHeight = -32;
39
+ let lineStartY: number = -35;
40
+ let lineEndY: number = -34;
41
+
42
+ if (tickmarkSize === TickmarkType.secondary) {
43
+ lineStartY = -24;
44
+ lineEndY = lineStartY + 8;
45
+ } else if (tickmarkSize === TickmarkType.main) {
46
+ lineEndY = lineStartY + 20;
47
+ } else if (tickmarkSize === TickmarkType.tertiary) {
48
+ throw new Error('Tertiary tickmarks are not supported');
49
+ }
50
+
51
+ const colorName = tickmarkColor(style);
52
+ const tick = svg`<line x1=${angle} y1=${lineStartY} x2=${angle} y2=${lineEndY} stroke=${colorName} stroke-width="1" vector-effect="non-scaling-stroke"/>`;
53
+ if (text) {
54
+ const textY = lineEndY + textHeight;
55
+ return [
56
+ tick,
57
+ svg`<text x=${angle} y=${textY} class="label" text-anchor="middle">${text}</text>`,
58
+ ];
59
+ }
60
+
61
+ return tick;
62
+ }
@@ -0,0 +1,19 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ .label {
6
+ @mixin font-body;
7
+ font-size: calc(16px / var(--scale));
8
+ fill: var(--element-neutral-color);
9
+ alignment-baseline: middle;
10
+ text-anchor: middle;
11
+ }
12
+
13
+ .label.left {
14
+ text-anchor: start;
15
+ }
16
+
17
+ .label.right {
18
+ text-anchor: end;
19
+ }
@@ -0,0 +1,17 @@
1
+ import type {Meta, StoryObj} from '@storybook/web-components';
2
+ import {ObcWatchFlat} from './watch-flat';
3
+ import './watch-flat';
4
+
5
+ const meta: Meta<typeof ObcWatchFlat> = {
6
+ title: 'Building blocks/Watch flat',
7
+ tags: ['autodocs'],
8
+ component: 'obc-watch-flat',
9
+ args: {},
10
+ } satisfies Meta<ObcWatchFlat>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<ObcWatchFlat>;
14
+
15
+ export const Primary: Story = {
16
+ args: {},
17
+ };
@@ -0,0 +1,148 @@
1
+ import {LitElement, SVGTemplateResult, html, svg, unsafeCSS} from 'lit';
2
+ import {customElement, property} from 'lit/decorators.js';
3
+ import compentStyle from './watch-flat.css?inline';
4
+ import {Tickmark, TickmarkStyle, tickmark} from './tickmark-flat';
5
+ import {rect} from '../../svghelpers/rectangular';
6
+ import {Label} from '../compass-flat/compass-flat';
7
+
8
+ @customElement('obc-watch-flat')
9
+ export class ObcWatchFlat extends LitElement {
10
+ @property({type: Number}) width = 352;
11
+ @property({type: Number}) height = 72;
12
+ @property({type: Number}) padding = 0;
13
+ @property({type: Number}) rotation = 0;
14
+ @property({type: Number}) tickmarkSpacing = 0;
15
+ @property({type: Number}) angleSetpoint: number | undefined;
16
+ @property({type: Array, attribute: false}) tickmarks: Tickmark[] = [];
17
+ @property({type: Array, attribute: false}) labels: Label[] = [];
18
+ @property({type: Array, attribute: false}) FOVIndicator: SVGTemplateResult[] =
19
+ [];
20
+ @property({type: Number}) trackHeight = (2 / 3) * this.height;
21
+ @property({type: Number}) ticksHeight = this.height - this.trackHeight;
22
+ @property({type: Number}) borderRadius = 8;
23
+
24
+ private renderClipPath(offsetY: number = 0): SVGTemplateResult {
25
+ return svg`
26
+ <clipPath id="frameClipPath${offsetY === 0 ? '' : 'Tickmarks'}">
27
+ <rect x="${-this.width / 2}" y="${-this.height / 2 + offsetY}"
28
+ width="${this.width}" height="${this.height}"
29
+ rx="${this.borderRadius}" />
30
+ </clipPath>
31
+ `;
32
+ }
33
+
34
+ private renderLabelMask(): SVGTemplateResult {
35
+ return svg`
36
+ <mask id="labelMask">
37
+ <rect x="${-this.width / 2}" y="${-70}"
38
+ width="${this.width}" height="${32}"
39
+ />
40
+ <linearGradient id="fadeGradient" gradientUnits="userSpaceOnUse"
41
+ x1="${-this.width / 2}" y1="0" x2="${this.width / 2}" y2="0">
42
+ <stop offset="0%" style="stop-color:black; stop-opacity:1;" />
43
+ <stop offset="10%" style="stop-color:white; stop-opacity:1;" />
44
+ <stop offset="50%" style="stop-color:white; stop-opacity:1;" />
45
+ <stop offset="90%" style="stop-color:white; stop-opacity:1;" />
46
+ <stop offset="100%" style="stop-color:black; stop-opacity:1;" />
47
+ </linearGradient>
48
+ <rect x="${-this.width / 2}" y="${-70}"
49
+ width="${this.width}" height="${32}"
50
+ fill="url(#fadeGradient)" />
51
+ </mask>
52
+ `;
53
+ }
54
+
55
+ private renderLabels(scale: number): SVGTemplateResult[] {
56
+ const labels: SVGTemplateResult[] = [];
57
+
58
+ for (const l of this.labels) {
59
+ labels.push(
60
+ svg`<g transform="translate(${-this.rotation * this.tickmarkSpacing}, ${-6 / scale})">
61
+ <text x=${l.x} y=${l.y} class="label" fill=${'var(--instrument-tick-mark-secondary-color)'}>
62
+ ${l.text}
63
+ </text>
64
+ </g>`
65
+ );
66
+ }
67
+
68
+ return labels;
69
+ }
70
+
71
+ private watchFace(): SVGTemplateResult {
72
+ const strokeWidth = 1;
73
+
74
+ return svg`
75
+ ${this.renderClipPath()}
76
+ ${this.renderClipPath(-40)}
77
+ <g clip-path="url(#frameClipPath)">
78
+ ${rect('frame-track', {
79
+ width: this.width,
80
+ height: this.trackHeight,
81
+ y: this.height / 2 - this.trackHeight,
82
+ strokeWidth: strokeWidth,
83
+ strokeColor: 'var(--instrument-frame-secondary-color)',
84
+ strokePosition: 'inside',
85
+ fillColor: 'var(--instrument-frame-secondary-color)',
86
+ borderRadius: 0,
87
+ })}
88
+ ${rect('frame-ticks', {
89
+ width: this.width,
90
+ height: this.ticksHeight,
91
+ y: this.height / 2 - this.trackHeight - this.ticksHeight,
92
+ strokeWidth: strokeWidth,
93
+ strokeColor: 'var(--instrument-frame-primary-color)',
94
+ strokePosition: 'inside',
95
+ fillColor: 'var(--instrument-frame-primary-color)',
96
+ borderRadius: 0,
97
+ })}
98
+ </g>
99
+ ${rect('frame-outline', {
100
+ width: this.width,
101
+ height: this.height,
102
+ strokeWidth: strokeWidth,
103
+ strokeColor: 'var(--instrument-frame-tertiary-color)',
104
+ strokePosition: 'inside',
105
+ fillColor: 'none',
106
+ borderRadius: this.borderRadius,
107
+ })}
108
+ `;
109
+ }
110
+
111
+ override render() {
112
+ const width = (this.width / 2 + this.padding) * 2;
113
+ const viewBox = `-${width / 2} -${this.height / 2} ${width} ${this.height}`;
114
+ const scale = this.clientWidth / width;
115
+
116
+ return html`
117
+ <svg
118
+ width="100%"
119
+ height="100%"
120
+ viewBox=${viewBox}
121
+ style="--scale: ${scale}"
122
+ >
123
+ ${this.watchFace()} ${this.renderLabelMask()} ${this.FOVIndicator}
124
+
125
+ <g clip-path="url(#frameClipPath)">
126
+ ${this.tickmarks.map(
127
+ (t) => svg`
128
+ <g transform="translate(${-this.rotation * this.tickmarkSpacing}, 0)">
129
+ ${tickmark(t.angle, t.type, TickmarkStyle.hinted)}
130
+ </g>
131
+ `
132
+ )}
133
+ </g>
134
+
135
+ <g mask="url(#labelMask)">
136
+ ${this.renderLabels(scale)}
137
+ </svg>
138
+ `;
139
+ }
140
+
141
+ static override styles = unsafeCSS(compentStyle);
142
+ }
143
+
144
+ declare global {
145
+ interface HTMLElementTagNameMap {
146
+ 'obc-watch-flat': ObcWatchFlat;
147
+ }
148
+ }