<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Özge Güner</title>
    <description>The latest articles on DEV Community by Özge Güner (@ozgesolidkey).</description>
    <link>https://dev.to/ozgesolidkey</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4172071%2F698705a7-a593-426f-a113-f4226a0e8181.png</url>
      <title>DEV Community: Özge Güner</title>
      <link>https://dev.to/ozgesolidkey</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ozgesolidkey"/>
    <language>en</language>
    <item>
      <title>Anime-style action FX in Compose Multiplatform, no shaders, one codebase</title>
      <dc:creator>Özge Güner</dc:creator>
      <pubDate>Thu, 08 Oct 2026 20:51:10 +0000</pubDate>
      <link>https://dev.to/solidkey/anime-style-action-fx-in-compose-multiplatform-no-shaders-one-codebase-2m6g</link>
      <guid>https://dev.to/solidkey/anime-style-action-fx-in-compose-multiplatform-no-shaders-one-codebase-2m6g</guid>
      <description>&lt;p&gt;You know the look. A sword swing trails a glowing ribbon of water. A bolt of lightning forks across the frame. Speed lines snap inward on the impact beat, and a shower of embers drifts up behind it. That is anime action, and it usually implies a game engine, platform shaders, or a pile of dependencies.&lt;/p&gt;

&lt;p&gt;This post builds that look in plain Compose Multiplatform. The exact same Kotlin runs on Android and iOS, there are no shaders, and nothing extra is pulled into your app. Every snippet is from &lt;a href="https://github.com/SolidKeyAB/kmpmedia" rel="noopener noreferrer"&gt;KMPMedia&lt;/a&gt;, an open-source library on Maven Central:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"se.solidkey:kmpmedia-lib:1.32.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The constraint that shapes everything
&lt;/h2&gt;

&lt;p&gt;The usual way to get rich motion FX is a fragment or pixel shader (RuntimeShader on Android 33+, SkSL on iOS). Shaders are powerful, but they cost you two things that matter for a multiplatform app. They split the codebase, because now you maintain shader code with its own language and its own capability floor per platform. And they are not deterministic across GPUs, so the "same" effect can look subtly different from device to device.&lt;/p&gt;

&lt;p&gt;There is another path. Keep the math on the CPU, in shared &lt;code&gt;commonMain&lt;/code&gt;, and let Compose's &lt;code&gt;Canvas&lt;/code&gt; and &lt;code&gt;graphicsLayer&lt;/code&gt; do the GPU compositing. Two rules make that fast enough:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parse once, interpolate per frame.&lt;/strong&gt; Build the heavy geometry a single time, then only lerp points each frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glow by addition, not blur.&lt;/strong&gt; A soft halo is a few additive passes of the same shape, which the GPU composites for free.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Follow those two rules and you get 60fps, byte-for-byte determinism, and one codebase. Everything below does exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  A slash ribbon
&lt;/h2&gt;

&lt;p&gt;The hero effect is a tapered ribbon drawn along a spline, with a draw-on reveal and a flowing gradient. The centreline is a centripetal Catmull-Rom spline (so it never loops or cusps on sharp input), sampled once; each frame only rebuilds the outline and advances the reveal. The gradient is interpolated in OKLab rather than sRGB, so the middle of a blue-to-navy ramp stays saturated instead of going grey. The edge wobble comes from simplex noise plus fBm.&lt;/p&gt;

&lt;p&gt;Three presets are one primitive wearing three elements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.Modifier&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.foundation.layout.fillMaxSize&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.solidkey.painpoints.fx.OGSlashView&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.solidkey.painpoints.fx.OGSlashes&lt;/span&gt;

&lt;span class="c1"&gt;// Water: a rippling blue sweep.&lt;/span&gt;
&lt;span class="nc"&gt;OGSlashView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OGSlashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WATER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;// Flame and Thunder are the same call, different data.&lt;/span&gt;
&lt;span class="nc"&gt;OGSlashView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OGSlashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FLAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;OGSlashView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OGSlashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;THUNDER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A slash is just data, so you can tweak a preset or author your own. Copy and adjust the fields you care about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;myWater&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OGSlashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WATER&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.14f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;colorStart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#9BE7FF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;colorEnd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#1E6FFF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"wave"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// "smooth" | "wave" | "rough" | "bolt"&lt;/span&gt;
    &lt;span class="n"&gt;glow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;revealMs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;220f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// the draw-on time&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;OGSlashView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;myWater&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the spec is a plain serializable type, it also round-trips through JSON. &lt;code&gt;OGSlashes.slashPrompt("a slow turquoise water arc")&lt;/code&gt; hands a language model the exact contract, and &lt;code&gt;OGSlashes.decodeSpec(reply)&lt;/code&gt; turns the reply back into a spec. A designer or an assistant can ship a &lt;code&gt;.slash&lt;/code&gt; pack with no code and no rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forked lightning
&lt;/h2&gt;

&lt;p&gt;Lightning is a recursive midpoint-displacement channel with branches, flickered over a short strike window. The jaggedness and the branch anchors are seeded, so the bolt is identical on both platforms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.solidkey.painpoints.fx.OGLightningView&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.solidkey.painpoints.fx.OGLightnings&lt;/span&gt;

&lt;span class="nc"&gt;OGLightningView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OGLightnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BOLT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="c1"&gt;// Also: OGLightnings.STORM (multi-strike) and OGLightnings.SPARK (small, fast).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Speed lines
&lt;/h2&gt;

&lt;p&gt;The manga impact frame: radial lines converging on a point, pulsing. Dirt cheap, and it sells motion more than almost anything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.solidkey.painpoints.fx.OGSpeedLinesView&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.solidkey.painpoints.fx.OGSpeedLines&lt;/span&gt;

&lt;span class="nc"&gt;OGSpeedLinesView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OGSpeedLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IMPACT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Also: OGSpeedLines.FOCUS (converge on a subject) and OGSpeedLines.MOTION (directional).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Elemental particles
&lt;/h2&gt;

&lt;p&gt;The particle system is a struct-of-arrays simulation: positions, sizes and colours live in flat &lt;code&gt;FloatArray&lt;/code&gt; and &lt;code&gt;IntArray&lt;/code&gt; buffers, so there is no per-frame allocation and a hard cap on live particles. It is deterministic from a seed, which is what lets the compositor export an identical GIF later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.solidkey.painpoints.particle.OGParticleView&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.solidkey.painpoints.particle.OGParticles&lt;/span&gt;

&lt;span class="nc"&gt;OGParticleView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OGParticles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EMBERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="c1"&gt;// Elemental presets: EMBERS, PETALS, DROPLETS, LEAVES.&lt;/span&gt;
&lt;span class="c1"&gt;// Classic presets: CONFETTI, SPARKS, SNOW, BOKEH, RAIN, FIREWORKS.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Glow and bloom
&lt;/h2&gt;

&lt;p&gt;There are two ways to make something luminous, and they make a deliberate trade.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Modifier.ogGlow&lt;/code&gt; is the portable one: it draws a few additive copies of the content, wide and faint through tight and bright, for a halo that hugs the silhouette. No blur, no API floor, pixel-identical on both platforms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.graphics.Color&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.solidkey.painpoints.fx.ogGlow&lt;/span&gt;

&lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ogGlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xFFFFE08A&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;intensity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Blade&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Modifier.ogBloom&lt;/code&gt; is the cinematic one: it redraws the content through a real platform gaussian blur and blends it additively. It looks softer and richer, but it is opt-in, because the blur needs Android 31+ (it no-ops to a crisp draw below that) and is not pixel-identical across platforms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.ui.unit.dp&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.solidkey.painpoints.fx.ogBloom&lt;/span&gt;

&lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ogBloom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intensity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Blade&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule of thumb: reach for &lt;code&gt;ogGlow&lt;/code&gt; when you need the result to be identical everywhere (including an export), and &lt;code&gt;ogBloom&lt;/code&gt; when you just want it to look its best on a modern device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Composing the beat
&lt;/h2&gt;

&lt;p&gt;Each effect is its own composable, so a full action beat is a stack. Here is a water-surface slash: speed lines framing the hit, the ribbon itself, and droplets trailing off it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;OGSpeedLinesView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OGSpeedLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IMPACT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;OGSlashView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OGSlashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WATER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nc"&gt;OGParticleView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OGParticles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DROPLETS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop the same layers onto the library's &lt;code&gt;OGComposition&lt;/code&gt; timeline instead of a &lt;code&gt;Box&lt;/code&gt;, keyframe their position and opacity, and you can export the whole thing to an animated GIF or a real H.264 MP4 on the device, with &lt;code&gt;OGComposition.exportGif()&lt;/code&gt; and &lt;code&gt;exportMp4()&lt;/code&gt;. The compositing math lives in tested &lt;code&gt;commonMain&lt;/code&gt;, so the GIF comes out byte-identical on both platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it stays at 60fps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The slash centreline, the lightning channel and the particle buffers are built once in &lt;code&gt;remember&lt;/code&gt; and only interpolated per frame. No re-parse, no per-frame allocation.&lt;/li&gt;
&lt;li&gt;Glow is additive blend, not a blur, so it is effectively free on the GPU.&lt;/li&gt;
&lt;li&gt;The transforms ride a single &lt;code&gt;graphicsLayer&lt;/code&gt;, so moving an effect does not trigger recomposition.&lt;/li&gt;
&lt;li&gt;Everything is seeded and deterministic, which is both why it looks identical on Android and iOS and why the export can be byte-for-byte reproducible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Grounded in research, not vibes
&lt;/h2&gt;

&lt;p&gt;None of this is ad-hoc. Each technique maps to a named result: OKLab (Ottosson, 2020) for the perceptual gradient, simplex noise plus fBm (Perlin, 2002; Gustavson; Mandelbrot) for the organic edges, recursive midpoint displacement (Fournier, Fussell, Carpenter, 1982) for the lightning, and median-cut plus Floyd-Steinberg plus LZW for the GIF encoder. The full map from feature to API to citation is in &lt;a href="https://github.com/SolidKeyAB/kmpmedia/blob/main/docs/ACADEMIC_FOUNDATIONS.md" rel="noopener noreferrer"&gt;ACADEMIC_FOUNDATIONS.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"se.solidkey:kmpmedia-lib:1.32.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library is MIT, on Maven Central, and runs on Android and iOS from one codebase. There is a Compose Multiplatform demo app that exercises every effect above, with live sliders and a JSON editor, and prebuilt APKs on its releases page.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/SolidKeyAB/kmpmedia" rel="noopener noreferrer"&gt;https://github.com/SolidKeyAB/kmpmedia&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo: &lt;a href="https://github.com/SolidKeyAB/kmpmedia-demo/releases" rel="noopener noreferrer"&gt;https://github.com/SolidKeyAB/kmpmedia-demo/releases&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something with it, I would love to see it.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>compose</category>
      <category>android</category>
      <category>kotlinmultiplatform</category>
    </item>
  </channel>
</rss>
