@shipload/item-renderer 1.0.0-next.23 → 1.0.0-next.24

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipload/item-renderer",
3
- "version": "1.0.0-next.23",
3
+ "version": "1.0.0-next.24",
4
4
  "description": "Deterministic SVG rendering for Shipload items",
5
5
  "homepage": "https://github.com/shipload/toolkit/tree/master/packages/item-renderer",
6
6
  "repository": {
@@ -45,7 +45,7 @@
45
45
  "fonts:copy": "bun run scripts/copy-fonts.ts"
46
46
  },
47
47
  "dependencies": {
48
- "@shipload/sdk": "^1.0.0-next.23",
48
+ "@shipload/sdk": "^1.0.0-next.24",
49
49
  "@wharfkit/antelope": "1.2.0"
50
50
  },
51
51
  "devDependencies": {
package/src/index.ts CHANGED
@@ -32,9 +32,14 @@ export {tokens} from './tokens/index.ts'
32
32
  export type {Tokens} from './tokens/index.ts'
33
33
  export type {CategoryColorKey, TierColorKey} from './tokens/colors.ts'
34
34
 
35
- // Category icon primitive
36
- export {categoryIconSvg, categoryIconPath} from './primitives/category-icon.ts'
37
- export type {CategoryIconPathOpts, CategoryIconSvgOpts} from './primitives/category-icon.ts'
35
+ // Resource icon primitive
36
+ export {
37
+ resourceIcon,
38
+ resourceIconBody,
39
+ resourceIconCategories,
40
+ resourceIconSvg,
41
+ } from './primitives/resource-icon.ts'
42
+ export type {ResourceIconInlineOpts, ResourceIconSvgOpts} from './primitives/resource-icon.ts'
38
43
 
39
44
  // Item cell templates
40
45
  export {renderItemCell, itemCellGroup} from './templates/item-cell.ts'
@@ -61,4 +66,3 @@ export {
61
66
  type ResolvedModuleSlot,
62
67
  type ResolvedAttributeGroup,
63
68
  } from '@shipload/sdk'
64
- export type {CategoryIconShape} from '@shipload/sdk'
@@ -0,0 +1,242 @@
1
+ import {CATEGORY_LABELS, categoryColors, type ResourceCategory} from '@shipload/sdk'
2
+ import {el, escapeXml} from './svg.ts'
3
+
4
+ export const resourceIconCategories = [
5
+ 'ore',
6
+ 'crystal',
7
+ 'gas',
8
+ 'regolith',
9
+ 'biomass',
10
+ ] as const satisfies readonly ResourceCategory[]
11
+
12
+ export interface ResourceIconSvgOpts {
13
+ size?: number
14
+ title?: string
15
+ className?: string
16
+ }
17
+
18
+ export interface ResourceIconInlineOpts {
19
+ x: number
20
+ y: number
21
+ size: number
22
+ }
23
+
24
+ const OUTLINE = '#06142f'
25
+ const HILITE = '#f7fbff'
26
+ const OUTER_STROKE = 4
27
+ const DETAIL_STROKE = 2.5
28
+
29
+ function oreIcon(): string {
30
+ return [
31
+ el('path', {
32
+ d: 'M13 24 L25 11 L43 13 L55 27 L48 49 L27 56 L12 42 Z',
33
+ fill: categoryColors.ore,
34
+ stroke: OUTLINE,
35
+ 'stroke-width': OUTER_STROKE,
36
+ 'stroke-linejoin': 'round',
37
+ }),
38
+ el('path', {d: 'M25 11 L31 30 L13 24 Z', fill: '#e39a5e'}),
39
+ el('path', {d: 'M31 30 L43 13 L55 27 L41 32 Z', fill: '#b85b35'}),
40
+ el('path', {d: 'M31 30 L41 32 L48 49 L27 56 Z', fill: '#8f432e'}),
41
+ el('path', {
42
+ d: 'M31 30 L25 11 M31 30 L13 24 M31 30 L41 32 M41 32 L48 49 M31 30 L27 56',
43
+ fill: 'none',
44
+ stroke: OUTLINE,
45
+ 'stroke-width': 1.5,
46
+ 'stroke-linecap': 'round',
47
+ 'stroke-linejoin': 'round',
48
+ opacity: 0.28,
49
+ }),
50
+ el('path', {
51
+ d: 'M21 23 L27 18 M42 20 L48 27',
52
+ fill: 'none',
53
+ stroke: '#ffd29c',
54
+ 'stroke-width': DETAIL_STROKE,
55
+ 'stroke-linecap': 'round',
56
+ opacity: 0.72,
57
+ }),
58
+ ].join('')
59
+ }
60
+
61
+ function crystalIcon(): string {
62
+ return [
63
+ el('path', {
64
+ d: 'M32 6 L49 24 L39 58 L23 58 L14 25 Z',
65
+ fill: categoryColors.crystal,
66
+ stroke: OUTLINE,
67
+ 'stroke-width': OUTER_STROKE,
68
+ 'stroke-linejoin': 'round',
69
+ }),
70
+ el('path', {d: 'M32 6 L32 58 L14 25 Z', fill: '#1fb9e4'}),
71
+ el('path', {d: 'M32 6 L49 24 L32 58 Z', fill: '#8df0ff'}),
72
+ el('path', {d: 'M23 58 L32 32 L39 58 Z', fill: '#2f87d7', opacity: 0.72}),
73
+ el('path', {
74
+ d: 'M32 6 L32 58 M14 25 L32 32 L49 24 M23 58 L32 32 L39 58',
75
+ fill: 'none',
76
+ stroke: OUTLINE,
77
+ 'stroke-width': 1.5,
78
+ 'stroke-linecap': 'round',
79
+ 'stroke-linejoin': 'round',
80
+ opacity: 0.25,
81
+ }),
82
+ el('path', {
83
+ d: 'M25 20 L31 13 M38 19 L43 25',
84
+ fill: 'none',
85
+ stroke: HILITE,
86
+ 'stroke-width': DETAIL_STROKE,
87
+ 'stroke-linecap': 'round',
88
+ opacity: 0.78,
89
+ }),
90
+ ].join('')
91
+ }
92
+
93
+ function gasIcon(): string {
94
+ return [
95
+ el('circle', {
96
+ cx: 32,
97
+ cy: 32,
98
+ r: 23,
99
+ fill: categoryColors.gas,
100
+ stroke: OUTLINE,
101
+ 'stroke-width': OUTER_STROKE,
102
+ }),
103
+ el('path', {
104
+ d: 'M17 33 C21 20 38 18 44 27 C51 38 37 51 25 43',
105
+ fill: 'none',
106
+ stroke: '#7a48d9',
107
+ 'stroke-width': 7.5,
108
+ 'stroke-linecap': 'round',
109
+ }),
110
+ el('path', {
111
+ d: 'M21 33 C25 25 36 25 39 31 C43 38 35 44 28 39',
112
+ fill: 'none',
113
+ stroke: '#f0d7ff',
114
+ 'stroke-width': 5,
115
+ 'stroke-linecap': 'round',
116
+ }),
117
+ el('circle', {
118
+ cx: 44,
119
+ cy: 18,
120
+ r: 5,
121
+ fill: '#f0d7ff',
122
+ stroke: OUTLINE,
123
+ 'stroke-width': DETAIL_STROKE,
124
+ }),
125
+ el('circle', {
126
+ cx: 19,
127
+ cy: 47,
128
+ r: 4,
129
+ fill: '#f0d7ff',
130
+ stroke: OUTLINE,
131
+ 'stroke-width': DETAIL_STROKE,
132
+ }),
133
+ ].join('')
134
+ }
135
+
136
+ function regolithIcon(): string {
137
+ return [
138
+ el('path', {
139
+ d: 'M16 14 H45 L54 25 V46 L43 55 H17 L9 43 V24 Z',
140
+ fill: categoryColors.regolith,
141
+ stroke: OUTLINE,
142
+ 'stroke-width': OUTER_STROKE,
143
+ 'stroke-linejoin': 'round',
144
+ }),
145
+ el('path', {d: 'M16 14 H45 L54 25 L34 28 Z', fill: '#dec59a'}),
146
+ el('path', {d: 'M9 43 L34 28 L43 55 H17 Z', fill: '#9e7c55', opacity: 0.78}),
147
+ el('path', {
148
+ d: 'M16 14 L34 28 L54 25 M34 28 L43 55 M34 28 L9 43',
149
+ fill: 'none',
150
+ stroke: OUTLINE,
151
+ 'stroke-width': 1.5,
152
+ 'stroke-linecap': 'round',
153
+ 'stroke-linejoin': 'round',
154
+ opacity: 0.25,
155
+ }),
156
+ el('circle', {cx: 25, cy: 28, r: 4, fill: '#806248', stroke: OUTLINE, 'stroke-width': 1.5}),
157
+ el('circle', {cx: 42, cy: 40, r: 5, fill: '#8b6a4d', stroke: OUTLINE, 'stroke-width': 1.5}),
158
+ el('circle', {
159
+ cx: 22,
160
+ cy: 44,
161
+ r: 3,
162
+ fill: '#e7d0a9',
163
+ stroke: OUTLINE,
164
+ 'stroke-width': 1.25,
165
+ }),
166
+ el('path', {
167
+ d: 'M35 18 L45 23',
168
+ fill: 'none',
169
+ stroke: '#f0dbb7',
170
+ 'stroke-width': DETAIL_STROKE,
171
+ 'stroke-linecap': 'round',
172
+ opacity: 0.72,
173
+ }),
174
+ ].join('')
175
+ }
176
+
177
+ function biomassIcon(): string {
178
+ return [
179
+ el('path', {
180
+ d: 'M32 7 C37 14 42 17 50 16 C50 24 54 29 57 35 C51 39 48 44 47 52 C39 50 36 55 31 59 C27 52 21 50 13 53 C14 45 10 40 7 34 C13 29 14 23 12 16 C21 18 26 14 32 7 Z',
181
+ fill: categoryColors.biomass,
182
+ stroke: OUTLINE,
183
+ 'stroke-width': OUTER_STROKE,
184
+ 'stroke-linejoin': 'round',
185
+ }),
186
+ el('circle', {cx: 32, cy: 32, r: 13, fill: '#73ad49', stroke: OUTLINE, 'stroke-width': 2}),
187
+ el('circle', {cx: 27, cy: 28, r: 5, fill: '#a8db6f', stroke: OUTLINE, 'stroke-width': 1.5}),
188
+ el('circle', {cx: 38, cy: 34, r: 5, fill: '#2f6f35', stroke: OUTLINE, 'stroke-width': 1.5}),
189
+ el('circle', {cx: 30, cy: 41, r: 4, fill: '#89c85a', stroke: OUTLINE, 'stroke-width': 1.5}),
190
+ el('circle', {
191
+ cx: 40,
192
+ cy: 24,
193
+ r: 3,
194
+ fill: '#c9ef8a',
195
+ stroke: OUTLINE,
196
+ 'stroke-width': 1.25,
197
+ }),
198
+ ].join('')
199
+ }
200
+
201
+ const iconBodies: Record<ResourceCategory, string> = {
202
+ ore: oreIcon(),
203
+ crystal: crystalIcon(),
204
+ gas: gasIcon(),
205
+ regolith: regolithIcon(),
206
+ biomass: biomassIcon(),
207
+ }
208
+
209
+ export function resourceIconBody(category: ResourceCategory): string {
210
+ return iconBodies[category]
211
+ }
212
+
213
+ export function resourceIcon(category: ResourceCategory, opts: ResourceIconInlineOpts): string {
214
+ const scale = opts.size / 64
215
+ return el(
216
+ 'g',
217
+ {transform: `translate(${opts.x} ${opts.y}) scale(${scale})`, 'data-resource': category},
218
+ resourceIconBody(category)
219
+ )
220
+ }
221
+
222
+ export function resourceIconSvg(
223
+ category: ResourceCategory,
224
+ opts: ResourceIconSvgOpts = {}
225
+ ): string {
226
+ const size = opts.size ?? 64
227
+ const title = opts.title ?? `${CATEGORY_LABELS[category]} resource icon`
228
+ const children = `<title>${escapeXml(title)}</title>${resourceIconBody(category)}`
229
+ return el(
230
+ 'svg',
231
+ {
232
+ xmlns: 'http://www.w3.org/2000/svg',
233
+ width: size,
234
+ height: size,
235
+ viewBox: '0 0 64 64',
236
+ role: 'img',
237
+ class: opts.className,
238
+ 'aria-label': title,
239
+ },
240
+ children
241
+ )
242
+ }
@@ -1,8 +1,8 @@
1
1
  import type {ResolvedItem} from '@shipload/sdk'
2
- import {tierColors, categoryColors, categoryIconShapes} from '@shipload/sdk'
2
+ import {tierColors} from '@shipload/sdk'
3
3
  import {el} from '../primitives/svg.ts'
4
4
  import {text} from '../primitives/text.ts'
5
- import {categoryIconPath} from '../primitives/category-icon.ts'
5
+ import {resourceIcon} from '../primitives/resource-icon.ts'
6
6
  import {tokens} from '../tokens/index.ts'
7
7
 
8
8
  export interface ItemCellProps {
@@ -48,16 +48,11 @@ function cellInner(props: ItemCellProps): string {
48
48
  family: tokens.typography.display,
49
49
  })
50
50
  } else if (props.resolved.category) {
51
- const shape = categoryIconShapes[props.resolved.category]
52
- const color = categoryColors[props.resolved.category]
53
- const iconCy = size * 0.4
54
- content = categoryIconPath({
55
- shape,
56
- cx,
57
- cy: iconCy,
58
- size: size * 0.32,
59
- color,
60
- strokeWidth: 1.5,
51
+ const iconSize = Math.round(size * 0.66)
52
+ content = resourceIcon(props.resolved.category, {
53
+ x: (size - iconSize) / 2,
54
+ y: Math.round(size * 0.12),
55
+ size: iconSize,
61
56
  })
62
57
  } else if (props.resolved.icon) {
63
58
  content = text({
@@ -2,12 +2,11 @@ import type {ResolvedItem} from '@shipload/sdk'
2
2
  import {getStatDefinitions, categoryColors, formatLocation} from '@shipload/sdk'
3
3
  import type {CargoItem} from '../payload/codec.ts'
4
4
  import {panel} from '../primitives/panel.ts'
5
- import {iconHex} from '../primitives/icon-hex.ts'
5
+ import {resourceIcon} from '../primitives/resource-icon.ts'
6
6
  import {statBar} from '../primitives/stat-bar.ts'
7
7
  import {quantityBadge} from '../primitives/quantity-badge.ts'
8
8
  import {tokens} from '../tokens/index.ts'
9
9
  import {
10
- shortCode,
11
10
  formatMass,
12
11
  tierBorder,
13
12
  metaRowBlock,
@@ -96,12 +95,9 @@ export function renderResource(
96
95
  tone: identity,
97
96
  })
98
97
 
99
- const icon = iconHex({
100
- x: pad,
101
- y: pad + ICON_Y,
102
- color: identity,
103
- code: shortCode(resolved.itemId),
104
- })
98
+ const icon = resolved.category
99
+ ? resourceIcon(resolved.category, {x: pad, y: pad + ICON_Y - 2, size: 28})
100
+ : ''
105
101
 
106
102
  const name = titleText(pad + 34, pad + 22, resolved)
107
103
 
@@ -1,4 +1,4 @@
1
- import {tierColors} from '@shipload/sdk'
1
+ import {categoryColors, tierColors} from '@shipload/sdk'
2
2
 
3
3
  export const colors = {
4
4
  surface: {
@@ -18,13 +18,7 @@ export const colors = {
18
18
  teal: '#2fd6d1',
19
19
  cyan: '#6cb9ff',
20
20
  },
21
- category: {
22
- ore: '#C26D3F',
23
- crystal: '#4ADBFF',
24
- gas: '#B8E4A0',
25
- regolith: '#C4A57B',
26
- biomass: '#5A8B3E',
27
- },
21
+ category: categoryColors,
28
22
  tier: tierColors,
29
23
  accent: {
30
24
  component: '#7E93C4',
@@ -1,110 +0,0 @@
1
- import type {CategoryIconShape} from '@shipload/sdk'
2
- import {el} from './svg.ts'
3
-
4
- export interface CategoryIconPathOpts {
5
- shape: CategoryIconShape
6
- cx: number
7
- cy: number
8
- size: number
9
- color: string
10
- strokeWidth?: number
11
- }
12
-
13
- export interface CategoryIconSvgOpts {
14
- size?: number
15
- color?: string
16
- strokeWidth?: number
17
- }
18
-
19
- function hexPoints(cx: number, cy: number, r: number): string {
20
- const pts: string[] = []
21
- for (let i = 0; i < 6; i++) {
22
- const angle = (Math.PI / 3) * i - Math.PI / 2
23
- pts.push(
24
- `${(cx + r * Math.cos(angle)).toFixed(2)},${(cy + r * Math.sin(angle)).toFixed(2)}`
25
- )
26
- }
27
- return pts.join(' ')
28
- }
29
-
30
- function diamondPoints(cx: number, cy: number, r: number): string {
31
- return [
32
- `${cx.toFixed(2)},${(cy - r).toFixed(2)}`,
33
- `${(cx + r).toFixed(2)},${cy.toFixed(2)}`,
34
- `${cx.toFixed(2)},${(cy + r).toFixed(2)}`,
35
- `${(cx - r).toFixed(2)},${cy.toFixed(2)}`,
36
- ].join(' ')
37
- }
38
-
39
- function starPoints(cx: number, cy: number, r: number): string {
40
- const inner = r * 0.45
41
- const pts: string[] = []
42
- for (let i = 0; i < 10; i++) {
43
- const angle = (Math.PI / 5) * i - Math.PI / 2
44
- const radius = i % 2 === 0 ? r : inner
45
- pts.push(
46
- `${(cx + radius * Math.cos(angle)).toFixed(2)},${(cy + radius * Math.sin(angle)).toFixed(2)}`
47
- )
48
- }
49
- return pts.join(' ')
50
- }
51
-
52
- export function categoryIconPath({
53
- shape,
54
- cx,
55
- cy,
56
- size,
57
- color,
58
- strokeWidth,
59
- }: CategoryIconPathOpts): string {
60
- const r = size / 2
61
- const stroked = strokeWidth && strokeWidth > 0
62
- const shapeAttrs = stroked
63
- ? {
64
- fill: 'none',
65
- stroke: color,
66
- 'stroke-width': strokeWidth,
67
- 'stroke-linejoin': 'round' as const,
68
- }
69
- : {fill: color}
70
- switch (shape) {
71
- case 'hex':
72
- return el('polygon', {points: hexPoints(cx, cy, r), ...shapeAttrs})
73
- case 'diamond':
74
- return el('polygon', {points: diamondPoints(cx, cy, r), ...shapeAttrs})
75
- case 'star':
76
- return el('polygon', {points: starPoints(cx, cy, r), ...shapeAttrs})
77
- case 'circle':
78
- return el('circle', {cx, cy, r, ...shapeAttrs})
79
- case 'square':
80
- return el('rect', {x: cx - r, y: cy - r, width: size, height: size, ...shapeAttrs})
81
- }
82
- const _exhaustive: never = shape
83
- throw new Error(`Unknown CategoryIconShape: ${String(_exhaustive)}`)
84
- }
85
-
86
- export function categoryIconSvg(shape: CategoryIconShape, opts: CategoryIconSvgOpts = {}): string {
87
- const size = opts.size ?? 16
88
- const color = opts.color ?? '#ffffff'
89
- const cx = size / 2
90
- const cy = size / 2
91
- const iconSize = size * 0.85
92
- const inner = categoryIconPath({
93
- shape,
94
- cx,
95
- cy,
96
- size: iconSize,
97
- color,
98
- strokeWidth: opts.strokeWidth,
99
- })
100
- return el(
101
- 'svg',
102
- {
103
- xmlns: 'http://www.w3.org/2000/svg',
104
- width: size,
105
- height: size,
106
- viewBox: `0 0 ${size} ${size}`,
107
- },
108
- inner
109
- )
110
- }