Skip to main content
ClaudeWave
Skill1.2k estrellas del repoactualizado 3d ago

compose-graphics

Advanced Compose visuals - Material 3 Expressive motion physics, AGSL shaders (Android 13+), Canvas/DrawScope generative, graphicsLayer effects.

Instalar en Claude Code
Copiar
git clone --depth 1 https://github.com/Jwuthri/Tracely-ai /tmp/compose-graphics && cp -r /tmp/compose-graphics/.agents/skills/compose-graphics ~/.claude/skills/compose-graphics
Después abre una sesión nueva de Claude Code; el skill carga automáticamente.

SKILL.md

# Compose Graphics

> Advanced Compose visuals: M3 Expressive motion physics, AGSL shaders (Android 13+), Canvas / DrawScope, graphicsLayer effects.
> Loaded for advanced thesis (shader, expressive, M3 Expressive, AGSL, Canvas, holographic).
> Foundation: `../compose-motion/SKILL.md` covers basics. Concise rules here. Deep-dive in `references/`.

---

## Decision Tree: Which API for Which Need

| Need | API |
|---|---|
| Spring physics with bounce / overshoot | `MotionScheme.expressive()` (M3 Expressive) |
| Pixel-level shader | `RuntimeShader` + `Modifier.graphicsLayer { renderEffect = ... }` (Android 13+) |
| Generative drawing (paths, particles, fractals) | `Canvas { drawScope -> ... }` |
| GPU effects (blur, shadows, color filters) | `Modifier.graphicsLayer { renderEffect = ... }` or `Modifier.blur(...)` |
| Adaptive system materials (Material You glassmorphism) | `Modifier.background(MaterialTheme.colorScheme.surfaceContainerHighest)` |
| Liquid glass on Android | AGSL shader recipe (no native API like iOS yet) |

---

## Domain 1: Material 3 Expressive

### What It Is

The 2025 Material 3 evolution introduces spring-based motion physics replacing fixed-duration tweens. New shape morphing API via `androidx.graphics.shapes`. New `MotionScheme` selectable on the theme. Aimed at hero moments, key interactions, brand-defining UI.

### MotionScheme

| Scheme | Personality | Use For |
|---|---|---|
| `MotionScheme.standard()` | Calmer, less overshoot | Default for chrome, lists, navigation |
| `MotionScheme.expressive()` | More overshoot, longer settle | Hero reveals, FABs, primary CTAs |

Apply on the theme:

```kotlin
MaterialTheme(motionScheme = MotionScheme.expressive()) {
    // children read tokens via MaterialTheme.motionScheme.*
}
```

Tokens exposed:

| Token | Domain | Speed |
|---|---|---|
| `fastSpatialSpec()` | Position / size | < 200ms |
| `defaultSpatialSpec()` | Position / size | ~ 350ms |
| `slowSpatialSpec()` | Position / size | ~ 600ms |
| `fastEffectsSpec()` | Opacity / color | < 150ms |
| `defaultEffectsSpec()` | Opacity / color | ~ 250ms |
| `slowEffectsSpec()` | Opacity / color | ~ 400ms |

> **Spatial vs Effects:** spatial = anything physical (height, offset, scale). Effects = visual properties without inertia (alpha, color, elevation). Springs feel natural for spatial; tweens feel right for effects. The tokens encode this for you.

### Hero Card Expand (Expressive Springs)

```kotlin
@Composable
fun ExpressiveHero() {
    var expanded by remember { mutableStateOf(false) }
    MaterialTheme(motionScheme = MotionScheme.expressive()) {
        val transition = updateTransition(targetState = expanded, label = "expand")
        val height by transition.animateDp(
            transitionSpec = { MaterialTheme.motionScheme.slowSpatialSpec() },
            label = "height"
        ) { if (it) 400.dp else 100.dp }
        val alpha by transition.animateFloat(
            transitionSpec = { MaterialTheme.motionScheme.defaultEffectsSpec() },
            label = "alpha"
        ) { if (it) 1f else 0f }

        Card(
            modifier = Modifier
                .fillMaxWidth()
                .height(height)
                .clickable { expanded = !expanded }
        ) {
            Box(modifier = Modifier.alpha(alpha)) {
                Text("Detail content", modifier = Modifier.padding(24.dp))
            }
        }
    }
}
```

### Spring Tuning Recipes

| Mood | Spec |
|---|---|
| Hero reveal | `spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)` |
| Snappy expressive | `spring(stiffness = Spring.StiffnessMediumLow, dampingRatio = 0.7f)` |
| Calm spatial | `MaterialTheme.motionScheme.defaultSpatialSpec()` (use the token) |
| Critical (no overshoot) | `spring(stiffness = Spring.StiffnessHigh, dampingRatio = 1f)` |

### Shape Morphing (M3 Expressive 1.3+)

`androidx.graphics.shapes` ships predefined morphable shapes (`MaterialShapes.Circle`, `Pentagon`, `Cookie4Sided`, `Sunny`, `Heart`, etc.) and a `Morph(start, end)` interpolator.

```kotlin
val morph = remember { Morph(MaterialShapes.Circle, MaterialShapes.Cookie4Sided) }
val progress by animateFloatAsState(
    targetValue = if (active) 1f else 0f,
    animationSpec = MaterialTheme.motionScheme.slowSpatialSpec(),
    label = "morph"
)
Box(
    modifier = Modifier
        .size(96.dp)
        .clip(GenericShape { size, _ ->
            addPath(
                morph.toPath(progress).asAndroidPath().asComposePath()
            )
        })
        .background(MaterialTheme.colorScheme.primary)
)
```

### When to Use Expressive vs Standard

- **Expressive:** hero moments, key interactions, FABs, primary CTAs. 1-3% of UI.
- **Standard:** default for the rest of the app. Mixing too much Expressive feels chaotic - every element fighting for attention.

---

## Domain 2: AGSL Shaders (Android 13+)

### What It Is

AGSL is Android's shader language. Similar to GLSL with simplifications (`half4` instead of `vec4`, restricted feature set, sandbox-safe). `RuntimeShader` compiles your AGSL source. Bind to a Compose modifier via `Modifier.graphicsLayer { renderEffect = ... }`.

### Setup

```kotlin
@Composable
fun ShaderEffect(content: @Composable () -> Unit) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
        // Android 12 and below: skip the shader, render content as-is.
        content()
        return
    }
    val shader = remember { RuntimeShader(AGSL_SOURCE) }
    val time by produceState(0f) {
        while (true) {
            withFrameMillis { ms ->
                value = ms / 1000f
            }
        }
    }
    Box(
        modifier = Modifier
            .onSizeChanged {
                shader.setFloatUniform("resolution", it.width.toFloat(), it.height.toFloat())
            }
            .graphicsLayer {
                shader.setFloatUniform("time", time)
                renderEffect = RenderEffect
                    .createRuntimeShaderEffect(shader,