Releases: SolidKeyAB/kmpmedia
Release list
v1.15.0 — Gesture + physics interactivity
KMPMedia 1.15.0 — Gesture + physics interactivity
Clip media to any shape — now grab, drag, pinch-zoom, twist-rotate, fling and spring it back. The interactivity half of Bet 4 — Interactivity primitives on the roadmap, productized out of the demo.
One modifier does both halves:
- Applies the pan / zoom / rotation through a single
graphicsLayer(a GPU-layer transform — no recomposition, holds 60fps). - Drives it from a custom
awaitEachGestureloop: one-/two-finger drag pans, pinch zooms (clamped), twist rotates, and on release the pan carries on with an exponential-decay momentum fling then springs back inside its bounds.
Pass an OGHitArea and the touch is shape-aware — a drag only starts inside the clip's silhouette, so the transparent corners of a triangle / circle / lasso fall through instead of grabbing it (and touches there pass to whatever is behind).
New API — com.solidkey.painpoints.gesture
Modifier.ogInteractive(state, hitArea?)— plus a convenience overload that remembers its own state.OGInteractionState/rememberOGInteractionState(...)— liveoffset/scale/rotationAnimatables you can read (HUD, other effects) +reset().OGInteractionConfig(presetsDragOnly/PanZoom/All) +OGPanBounds— size-independent pan limits (a fraction of content size, so one config works at any resolution).OGHitArea— a normalized0..1hit region:RECT/CIRCLE/TRIANGLE_UP/TRIANGLE_DOWN/DIAMOND/polygon(...)/of(OGShapeType).
Zero new dependency; identical on Android & iOS. Purely additive — a new package, nothing existing changed. The hit-test + clamp maths are pure and unit-tested on JVM + iOS (16 tests); live-verified on Android (drag in all directions, pan bounds + spring settle, shape-aware grab/ignore).
implementation("se.solidkey:kmpmedia-lib:1.15.0")See docs/INTERACTIVE.md and the full CHANGELOG.md. The demo's Interactive shapes screen dogfoods it.
v1.14.0 — Compositor export → H.264 MP4 🎬
Compositor export → H.264 MP4 🎬
The on-device compositor could already export a shareable animated GIF (1.13.0). 1.14.0 adds a second export target: the same OGComposition renders to a real H.264 MP4 with OGComposition.exportMp4() — driving the device's own video encoder, with no third-party dependency.
Added
OGComposition.exportMp4(bitRate = defaultMp4BitRate(width, height, fps), background = Color.Black)— renders every frame offscreen at the composition'sfps, flattens it onto an opaquebackground(H.264 has no alpha), and encodes a real H.264 MP4 with the OS video encoder: AndroidMediaCodec+MediaMuxer, iOSAVAssetWriter. Returns the encodedByteArray; saving/sharing stays the app's call.defaultMp4BitRate(width, height, fps)— a resolution/fps-scaled default average bit rate, clamped to a 0.75–16 Mbps band.
The byte stream isn't identical across platforms (each OS has its own encoder), but the input is: compositing and the RGB→YUV / RGB→BGRA colour maths live in shared, tested commonMain, so both encoders compress the same rendered frames. H.264 needs even dimensions. Purely additive — one new extension, nothing existing changed. Completes Bet 3 — on-device compositor + export beyond GIF.
Verified: 10 new tests green on JVM + iOS; Android live (MediaCodec → a valid 288² @20fps 40-frame H.264, played back through OGAVPlayer); iOS live (AVAssetWriter on the simulator → a valid standard High-profile H.264, ffprobe-confirmed). See docs/COMPOSITOR.md.
implementation("se.solidkey:kmpmedia-lib:1.14.0")v1.13.0 — On-device compositor + GIF export 📽️
On-device compositor + GIF export — the flagship "2.0" 📽️
KMPMedia could put one piece of media in one shape. 1.13.0 adds Bet 3: compose several layers on one timeline, animate them with keyframes, preview the result live at 60fps, and export it to a shareable animated GIF — all authored on device. No other KMP library does compose-and-export.
Added
OGComposition— a fixedwidth×heightcanvas, adurationMstimeline atfps, a background colour, and a back-to-front stack ofOGCompositionLayers. Each layer is a still image or a solid fill, placed in a box whose x / y / scale / rotation / opacity are each an animatable keyframe track, optionally clipped to any KMPMedia shape (built-in, lasso, multi-region,CircleShape, …), visible over[startMs, endMs].OGCompositionView— plays the composition on the Compose frame clock (holds 60fps, no intermediate bitmap) or acts as a scrubber viapositionMs.OGComposition.exportGif()— renders every frame offscreen and encodes one looping animated GIF with the built-in pure-KotlinOGGifEncoder(median-cut palette + LZW) — no dependency, no platform encoder, identical bytes on Android and iOS. Returns theByteArray; where to save/share is the app's call.renderFrame(timeMs)grabs any single frame.OGKeyframedFloat/OGKeyframe/OGEasing— an animatable float track (LINEAR / EASE_IN / EASE_OUT / EASE_IN_OUT), parse-nothing / lerp-per-frame.OGGifEncoder— a standalone, dependency-free GIF89a encoder usable on its own for any frame list.
Everything is pure commonMain and purely additive (new com.solidkey.painpoints.compositor package, nothing existing changed). 19 tests green on JVM + iOS. See docs/COMPOSITOR.md. MP4 / frame-sequence export are the planned follow-ups.
implementation("se.solidkey:kmpmedia-lib:1.13.0")v1.12.0 — Soft & gradient masks, multi-region clips
Bet 2 — Living shapes: soft & gradient masks, multi-region clips.
A clip mask no longer has to be one shape with a hard edge:
OGMultiRegionShape(com.solidkey.painpoints.shape) — combine several placed sub-shapes with a path op (union / intersect / difference / xor) into one clip: two portholes, or a diamond with a circular bite. A plain ComposeShape, so it drops into the existingclipShapeslot for image, GIF and video with zero consumer change. Outlines combined once per size, never per frame. Factoriesunion(...)/of(...);OGClipRegionplaces a sub-shape in a normalized 0..1 sub-rect.Modifier.ogSoftClip(shape, feather)+ogGradientMask(brush)(com.solidkey.painpoints.mask), surfaced onOGImageViewassoftEdge/maskBrush— feather a clip edge into a vignette, or fade content along a gradient, via one offscreenBlendMode.DstInpass (no blur, noRenderEffect, no Android API floor). Scoped to Compose-drawn content (photo/GIF); video stays hard-clipped.
Zero new dependencies. 13 new tests green on JVM + iOS. Purely additive — every existing call compiles unchanged.
implementation("se.solidkey:kmpmedia-lib:1.12.0")Docs: docs/SOFT_MASKS.md. See the README for live Android + iOS demo clips.
v1.11.0 — AI hooks: describe → shape / patch 🤖
AI hooks — "describe → shape / patch." KMPMedia's vector primitives are just data, so a language model can produce them. New com.solidkey.painpoints.ai.OGAiVector is a provider-agnostic JSON interop layer: it hands a model the exact contract and turns the reply back into a live clip shape or SVG patch.
polygonPrompt(instruction)/svgPatchPrompt(instruction, nodeIds)— build a prompt stating the JSON schema.decodePolygon/decodeSvgPatch(+…OrNull) — a model's reply →OGPolygonShapeforclipShape, orMap<String, OGSvgNodeOverride>forOGSVGView(overrides = …). Tolerant of the ```json fences + prose models add.encodePolygon/encodeSvgPatch— serialize the other way (persist a lasso, seed a prompt).OGPolygonSpec/OGNodeOverrideSpec/OGSvgPatchSpec—@SerializableDTOs.
No network calls and no AI SDK in the library — it defines the contract and does the JSON; you own the model and the call. Purely additive (new package), zero-dependency beyond the serialization family already in use. Colors reuse the SVG color parser (#RGB/#RRGGBB/#AARRGGBB/rgb()/name). All work happens at generate/patch time, never per frame. 12 tests green on JVM + iOS.
First shipped piece of Bet 1 — Runtime & AI-editable vector's "AI hooks" item. See docs/AI_HOOKS.md.
implementation("se.solidkey:kmpmedia-lib:1.11.0")v1.10.0 — Shape-morph clips (OGMorphShape) 🫧
Morph the clip mask itself
New this release: OGMorphShape — a Compose Shape whose outline morphs from one shape to another as you drive progress 0f → 1f. Because every KMPMedia media surface clips through a plain Shape (OGImageView.clipShape, OGPlayerConfig.clipShape, any Modifier.clip), it drops straight into that existing slot — so a live video, GIF or photo can be masked by an outline that animates circle → diamond → triangle → lasso while the media keeps playing, with no new surface API.
OGMorphShape(from, to, progress)— morph a mask between any two shapes (built-ins, aRoundedCornerShape, or anOGPolygonShapelasso).ogMorphSequence(stops, progress)— oneprogresswalks a whole chain of shapes for a seamless multi-stop loop.- Drive
progressfrom any Compose animation (animateFloatAsState, an infinite transition, a slider).
Built for 60fps (the perf + simplicity gate)
Each endpoint outline is resampled once into equally-spaced perimeter points and cached by (from, to, size); per frame only those two point lists are lerped — no re-measure, no allocation churn. At progress ≤ 0 / ≥ 1 the raw endpoint outline passes straight through, so a settled clip costs nothing. Mismatched winding is auto-aligned so shapes morph without swirling. Same sample-once / lerp-per-frame approach as the SVG path morph.
Install
implementation("se.solidkey:kmpmedia-lib:1.10.0")Identical on Android & iOS. See docs/SHAPE_MORPH_CLIPS.md. Tests green on JVM host + iOS; dogfooded in the demo's "Morph the clip itself" screen and the UFO Dodge shape-shifting hazard.
v1.9.0 — SVG path morphing
Path morphing — a <path> tweens smoothly between two shapes at runtime.
Set OGSvgNodeOverride.pathDataTo to a target d and drive morphProgress 0→1 from any Compose animation; the node interpolates from its current geometry to the target. Built for 60fps: both endpoint d strings are parsed once and cached, so each frame only lerps floats — no re-parse, no allocation churn. Structurally-mismatched paths snap at the halfway point rather than drawing garbage.
Purely additive (two new optional fields). Verified on Android + iOS (17 new unit tests) and dogfooded as a shape-shifting hazard in the demo's UFO Dodge game.
implementation("se.solidkey:kmpmedia-lib:1.9.0")
See docs/RUNTIME_SVG.md and CHANGELOG.
v1.8.0 — Runtime SVG path `d` override
Runtime path geometry. Runtime-editable SVG can now change a node's shape, not just its paint and transform. An OGSvgNodeOverride may carry a replacement path d, and the addressed <path> re-parses to the new geometry live — no new node, no re-parse of the whole SVG. This is the building block for path morphing (the next roadmap step).
Purely additive: a new optional pathData field defaulting to null, so every existing call is unchanged. Same code on Android and iOS.
Added
OGSvgNodeOverride.pathData: String?— a replacement pathd. When set on a node holding<path>geometry, the node redraws with this geometry instead of its original;null(default) keeps the original. Ignored on non-path nodes.
Notes
- The new
dis re-parsed against the SVG's viewBox (same user-space as the source), so the viewport transform and any paint/transform override on the node still apply on top. ReusesparsePathCommands; the resolve rebuilds a fresh shapes list and never mutates the parsed source tree. - Callers can compute a
dper frame (e.g. interpolate between two same-structure paths) as a stop-gap until first-class path-morphing lands.
implementation("se.solidkey:kmpmedia-lib:1.8.0")Cumulative superset — also includes SVG runtime overrides, image/GIF/video playback + clip-to-shape, depth-layer, polygon lasso, and audio sprites.
v1.7.0 — Runtime-editable SVG 🎨
🎨 Runtime-editable SVG
KMPMedia parses an SVG into a live node tree — so from 1.7.0 you can address any node by its id and change its attributes at runtime, bound to Compose state. A static .svg becomes a live template: gauges, charts, progress rings, status icons. The source is parsed once; only the overrides change and it redraws live. Same code on Android and iOS.
OGSVGView(
source = OGSvgUrlType("gauge.svg"),
width = 300f, height = 195f,
overrides = mapOf(
"needle" to OGSvgNodeOverride(rotation = angle, rotationCx = 100f, rotationCy = 105f),
"arc" to OGSvgNodeOverride(stroke = zoneColor),
"status" to OGSvgNodeOverride(fill = zoneColor),
),
)OGSvgNodeOverride— a per-node override; every field nullable (null= keep original):fill,stroke,strokeWidth,translateX/translateY,rotation(+rotationCx/rotationCypivot),scaleX/scaleY.- Change the
overridesmap from state → only the matched nodes re-resolve, no re-parse of the source. - Paint folds into the node's style; transforms apply uniformly at draw time, so they work on any shape type.
- Purely additive —
overridesdefaults to empty, so every existingOGSVGViewcall is unchanged.
Install
implementation("se.solidkey:kmpmedia-lib:1.7.0")Cumulative superset — also includes animated GIF playback, video/image playback & seek, depth-layer, polygon lasso, video clip-to-shape and audio sprites from prior releases.
📖 Docs: docs/RUNTIME_SVG.md · 🗺️ ROADMAP · 📝 CHANGELOG
🎥 See the live gauge (needle + zones driven by a slider, Android + iOS) in the demo app's Runtime-editable SVG screen.
v1.6.0 — Animated GIF 🎞️
🎞️ Animated GIF playback (Android + iOS)
KMPMedia now plays animated GIFs — on both Android and iOS, from the same one-liner. Before 1.6.0 the image path decoded only a GIF's first frame (a still picture); now the frames actually animate, looping, with the same API as any other image.
OGImageView(
source = OGImageUrlType("https://example.com/loading.gif"),
onEventTriggered = { _, _ -> },
onError = { },
)- Point
OGImageViewat a.gif(URL, file path, or iOS resource) and it plays — no API change to existing calls. - New
rememberOGAnimatedPainter(source, loop = true, speed = 1f, onError = null)incom.solidkey.painpoints.image.gifif you want the playingPainterdirectly. - The same shape clip that crops a still photo works on the moving frames too — a GIF animating inside a circle / diamond.
- No new dependencies — native decoders: Android
AnimatedImageDrawable(API 28+, static first-frame fallback on 24–27), iOS SkiaCodecdriven by a small, unit-tested frame clock (OGGifClock). - Not GIF-specific: animated WebP and other multi-frame formats ride the same path.
Install
implementation("se.solidkey:kmpmedia-lib:1.6.0")Cumulative superset — also includes the SVG + image/video playback & seek, depth-layer, polygon lasso, video clip-to-shape and audio-sprite features from prior releases.
📖 Docs: docs/GIF.md · 📝 CHANGELOG
🎥 See it in action (Android + iOS captures): the demo app "Animated GIF" screen.