Files
amethyst/.claude/skills/compose-expert/references/icon-assets.md
T
2025-12-30 15:45:02 +02:00

366 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Custom Icon Assets and ImageVector Patterns
Guide to creating and using custom ImageVector icons in Compose Multiplatform.
## Why ImageVector?
ImageVector is the native Compose format for vector graphics:
- **Pure Kotlin**: No XML, no asset files
- **Multiplatform**: Works on Android, Desktop, iOS without conversion
- **Performant**: Lightweight, composable, GPU-accelerated
- **Type-safe**: Compile-time checking, no resource IDs
## Amethyst Pattern: Robohash
Amethyst generates deterministic avatars using ImageVector builders.
### Architecture
```
commons/robohash/
├── RobohashAssembler.kt # Main assembly logic
├── CachedRobohash.kt # Caching layer
└── parts/
├── Face0C3po.kt # Face variants (0-9)
├── Eyes2Single.kt # Eye variants (0-9)
├── Mouth3Grid.kt # Mouth variants (0-9)
├── Body2Thinnest.kt # Body variants (0-9)
└── Accessory7Antenna.kt # Accessory variants (0-9)
```
**Pattern**: 10 variants per feature × 5 features = 100,000+ unique combinations
### roboBuilder DSL
Custom ImageVector builder with sensible defaults:
```kotlin
fun roboBuilder(block: Builder.() -> Unit): ImageVector {
return ImageVector.Builder(
name = "Robohash",
defaultWidth = 300.dp,
defaultHeight = 300.dp,
viewportWidth = 300f,
viewportHeight = 300f
).apply(block).build()
}
```
**Usage**:
```kotlin
@Composable
fun CustomIcon() {
Image(
painter = rememberVectorPainter(
roboBuilder {
// Add paths here
}
),
contentDescription = "Custom icon"
)
}
```
### Path Building Pattern
```kotlin
fun face0C3po(fgColor: SolidColor, builder: Builder) {
builder.addPath(pathData1, fill = fgColor, stroke = Black, strokeLineWidth = 1.5f)
builder.addPath(pathData2, fill = Black, fillAlpha = 0.4f)
builder.addPath(pathData5, fill = Black, fillAlpha = 0.2f)
builder.addPath(pathData6, stroke = Black, strokeLineWidth = 1.0f)
builder.addPath(pathData7, fill = Black, stroke = Black, fillAlpha = 0.2f, strokeLineWidth = 0.75f)
}
private val pathData1 = PathData {
moveTo(144.5f, 87.5f)
reflectiveCurveToRelative(-51.0f, 3.0f, -53.0f, 55.0f)
curveToRelative(0.0f, 0.0f, 0.0f, 27.0f, 5.0f, 42.0f)
reflectiveCurveToRelative(10.0f, 38.0f, 10.0f, 38.0f)
lineToRelative(16.0f, 16.0f)
// ...
close()
}
```
**Key elements**:
- `pathData` variables for path commands
- `addPath()` for each layer
- Parameterized colors (`fgColor`)
- Constant colors (`Black`)
- Alpha for shadows/highlights
### PathData DSL
Compose's PathData builder provides SVG-like commands:
| Command | Description | Example |
|---------|-------------|---------|
| `moveTo(x, y)` | Move pen without drawing | `moveTo(100f, 100f)` |
| `lineTo(x, y)` | Draw line to point | `lineTo(200f, 150f)` |
| `curveToRelative(...)` | Relative cubic Bézier | `curveToRelative(10f, 20f, 30f, 40f, 50f, 60f)` |
| `reflectiveCurveToRelative(...)` | Smooth curve | `reflectiveCurveToRelative(-51f, 3f, -53f, 55f)` |
| `horizontalLineTo(x)` | Horizontal line | `horizontalLineTo(250f)` |
| `verticalLineTo(y)` | Vertical line | `verticalLineTo(300f)` |
| `close()` | Close path | `close()` |
**Relative vs Absolute**:
- `moveTo` / `lineTo` - Absolute coordinates
- `moveToRelative` / `lineToRelative` - Relative to current position
## Creating Custom Icons
### Method 1: From SVG (Recommended)
1. **Export SVG** from design tool (Figma, Illustrator)
2. **Convert to ImageVector** using Android Studio's Vector Asset tool
3. **Extract path data** and adapt to roboBuilder pattern
```kotlin
// SVG path: M 10 10 L 20 20 ...
// Becomes:
private val myIconPath = PathData {
moveTo(10f, 10f)
lineTo(20f, 20f)
// ...
}
```
### Method 2: Programmatic
Build paths programmatically for simple shapes:
```kotlin
fun simpleIcon(): ImageVector = roboBuilder {
addPath(
pathData = PathData {
moveTo(50f, 50f)
lineTo(150f, 50f)
lineTo(150f, 150f)
lineTo(50f, 150f)
close()
},
fill = SolidColor(Color.Blue),
stroke = SolidColor(Color.Black),
strokeLineWidth = 2f
)
}
```
### Method 3: Material Icons Extensions
Extend Material Icons when you need platform-consistent icons:
```kotlin
// For standard icons, use Material Icons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
Icon(Icons.Default.Refresh, contentDescription = "Refresh")
Icon(Icons.Default.Check, contentDescription = "Success")
Icon(Icons.Default.Close, contentDescription = "Error")
```
## CachedRobohash Pattern
Performance optimization for generated icons:
```kotlin
object CachedRobohash {
private val cache = mutableMapOf<Pair<String, Boolean>, ImageVector>()
fun get(seed: String, isLight: Boolean): ImageVector {
return cache.getOrPut(seed to isLight) {
RobohashAssembler.assemble(seed, isLight)
}
}
}
```
**Pattern**:
- Key: `(seed, theme)` pair
- Value: Assembled ImageVector
- Lifecycle: Application lifetime (never cleared)
**Usage**:
```kotlin
@Composable
fun RobohashImage(robot: String) {
Image(
imageVector = CachedRobohash.get(robot, isLightTheme()),
contentDescription = "Avatar for $robot"
)
}
```
## Color Management
### Dynamic Colors
Pass colors as parameters for theme adaptation:
```kotlin
fun themedIcon(fgColor: SolidColor, bgColor: SolidColor, builder: Builder) {
builder.addPath(pathData1, fill = bgColor)
builder.addPath(pathData2, fill = fgColor)
}
@Composable
fun ThemedIcon() {
val fg = MaterialTheme.colorScheme.primary
val bg = MaterialTheme.colorScheme.surface
Image(
painter = rememberVectorPainter(
roboBuilder {
themedIcon(SolidColor(fg), SolidColor(bg), this)
}
),
contentDescription = null
)
}
```
### Static Colors
Define constants for colors that don't change:
```kotlin
val Black = SolidColor(Color.Black)
val White = SolidColor(Color.White)
val Transparent = SolidColor(Color.Transparent)
```
## Advanced Techniques
### Layering
Build complex icons with multiple layers:
```kotlin
fun complexIcon(builder: Builder) {
// Layer 1: Background
builder.addPath(bgPath, fill = SolidColor(Color.White))
// Layer 2: Shadow
builder.addPath(shadowPath, fill = SolidColor(Color.Black), fillAlpha = 0.2f)
// Layer 3: Main shape
builder.addPath(mainPath, fill = SolidColor(Color.Blue))
// Layer 4: Highlight
builder.addPath(highlightPath, fill = SolidColor(Color.White), fillAlpha = 0.3f)
// Layer 5: Stroke
builder.addPath(outlinePath, stroke = SolidColor(Color.Black), strokeLineWidth = 1f)
}
```
**Render order**: Bottom to top (first addPath = bottom layer)
### Alpha for Visual Effects
```kotlin
// Shadow
builder.addPath(shadowPath, fill = Black, fillAlpha = 0.4f)
// Highlight
builder.addPath(highlightPath, fill = White, fillAlpha = 0.2f)
// Glass effect
builder.addPath(glassPath, fill = White, fillAlpha = 0.1f)
```
### Stroke Styles
```kotlin
// Outline only
builder.addPath(path, stroke = Black, strokeLineWidth = 1.5f)
// Fill + outline
builder.addPath(path, fill = fgColor, stroke = Black, strokeLineWidth = 1f)
// Dashed (not supported directly, use multiple segments)
```
## Composable Icon Pattern
Wrap ImageVector in a Composable for reusability:
```kotlin
@Composable
fun MyCustomIcon(
modifier: Modifier = Modifier,
tint: Color = Color.Unspecified
) {
Image(
painter = rememberVectorPainter(myIconVector()),
contentDescription = "My custom icon",
modifier = modifier,
colorFilter = if (tint != Color.Unspecified) {
ColorFilter.tint(tint)
} else null
)
}
```
**Usage**:
```kotlin
MyCustomIcon(
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
```
## Best Practices
### DO
✅ Cache generated ImageVectors for performance
✅ Use PathData DSL for readability
✅ Parameterize colors for theme support
✅ Use Material Icons for standard icons
✅ Keep viewport size consistent (e.g., 300×300)
✅ Layer paths from back to front
✅ Use alpha for shadows and highlights
### DON'T
❌ Generate ImageVectors in @Composable without caching
❌ Hardcode theme-specific colors
❌ Create custom icons for standard Material icons
❌ Use extreme viewport sizes (stay 24-1000dp)
❌ Mix absolute and relative coordinates unnecessarily
❌ Forget to close() paths
## Icon Organization
### Structure
```
commons/icons/
├── CustomIcons.kt # Icon collection object
├── icons/
│ ├── Zap.kt # Lightning bolt
│ ├── Relay.kt # Relay indicator
│ └── Bitcoin.kt # Bitcoin symbol
└── builders/
└── IconBuilder.kt # Shared builder utilities
```
### Collection Object
```kotlin
object CustomIcons {
val Zap: ImageVector by lazy { ZapIcon.create() }
val Relay: ImageVector by lazy { RelayIcon.create() }
val Bitcoin: ImageVector by lazy { BitcoinIcon.create() }
}
// Usage
Icon(CustomIcons.Zap, contentDescription = "Zap")
```
## Resources
- [Compose ImageVector API](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/vector/ImageVector)
- [SVG Path Commands](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths)
- [Material Icons](https://fonts.google.com/icons)
- Robohash implementation: `commons/robohash/` in AmethystMultiplatform