DEV Community

Cover image for I built Geometric Breathing & Meditation app on VCA
YASHWANTH REDDY K
YASHWANTH REDDY K

Posted on

I built Geometric Breathing & Meditation app on VCA

The Animation Isn’t Breathing — Your Timing Logic Is

At a glance, this feels like one of those “calm UI” exercises.

A soft glowing shape.
Expand… hold… contract.
Maybe a gentle color palette and a faint sound.

It looks simple enough that most people jump straight into CSS animations.

And that’s exactly where things go wrong.

Because the moment you try to sync everything — motion, sound, counters, presets — you realize:

This is not an animation problem.
This is a timing system pretending to be one.

The illusion of “just use CSS animation”

The obvious move:

@keyframes breathe {
  0%   { transform: scale(1); }
  33%  { transform: scale(1.3); }
  66%  { transform: scale(1.3); }
  100% { transform: scale(1); }
}
Enter fullscreen mode Exit fullscreen mode

Looks perfect on paper.

  • expand (0 → 33%)
  • hold (33 → 66%)
  • contract (66 → 100%)

Done.

Until you try to:

  • play a sound exactly at inhale
  • increment a counter per full cycle
  • change speed dynamically
  • switch presets mid-cycle

Now your animation is running on one timeline…

…and your logic is guessing where it is.

This is where most implementations drift

You end up writing code that tries to “sync” with CSS:

setInterval(() => {
  playSound();
}, 4000);
Enter fullscreen mode Exit fullscreen mode

It works.

Until:

  • the tab loses focus
  • frame timing shifts
  • animation delays slightly

Now your sound is out of phase.

Your counter increments late.

Your “breathing” feels… mechanical.

The shift: stop syncing to animation

Start driving animation from time

Instead of asking:

“What is the animation doing right now?”

You flip it:

“What phase should we be in right now?”

You define time explicitly:

const PHASES = ["in", "hold", "out"];
const DURATION = 4000;
Enter fullscreen mode Exit fullscreen mode

And track real progression:

const elapsed = (Date.now() - startTime) % (DURATION * 3);
Enter fullscreen mode Exit fullscreen mode

Now you know exactly:

  • which phase you're in
  • how far into it you are

No guessing.

No syncing hacks.

The geometry isn’t decorative

It’s expressive

The requirement says:

6–8 segments forming a shape

Most implementations just duplicate elements.

But the interesting part is how they behave together.

A single petal scaling is boring.

Multiple petals slightly offset in rotation?

Now you get something organic.

.petal {
  transform: rotate(var(--angle)) scale(var(--scale));
}
Enter fullscreen mode Exit fullscreen mode

That --angle isn’t styling.

It’s structure.

The “bloom” effect is not about size

It’s about stagger

If all segments animate identically, the shape feels rigid.

If each segment has a slight delay or opacity variation…

It feels alive.

CSS variables quietly become your control panel

This is where the system gets interesting.

Instead of hardcoding values:

.petal {
  background: teal;
  transition: 4s;
}
Enter fullscreen mode Exit fullscreen mode

You externalize everything:

:root {
  --duration: 4s;
  --color: #4fd1c5;
}
Enter fullscreen mode Exit fullscreen mode

Now your entire animation becomes configurable.

And JavaScript doesn’t “change styles”.

It changes parameters.

Presets stop being themes

They become system reconfigurations

Switching to “Fire” isn’t just color.

It’s:

  • faster duration
  • sharper easing
  • higher contrast
document.documentElement.style.setProperty("--duration", "2s");
Enter fullscreen mode Exit fullscreen mode

That one line reshapes the entire experience.

The sound exposes your timing bugs instantly

Visual drift is subtle.

Audio drift is obvious.

If your “ding” doesn’t land exactly at:

  • inhale start
  • exhale start

The whole system feels off.

Which is why this matters:

if (phaseChanged) {
  playTone();
}
Enter fullscreen mode Exit fullscreen mode

You’re not triggering sound on intervals.

You’re triggering it on state transitions.

That’s a different mindset.

The breath counter is deceptively strict

It doesn’t count:

  • expansions
  • contractions

It counts full cycles

Which means:

if (phase === "in" && previousPhase === "out") {
  cycles++;
}
Enter fullscreen mode Exit fullscreen mode

Tiny detail.

But if you get this wrong…

Your app feels inaccurate.

And in a meditation context, that matters more than visuals.

What shows up in Vibe Code Arena

This challenge separates two kinds of thinking.

One model builds an animation:

  • smooth CSS
  • decent visuals
  • loosely synced JS

It looks calming.

But feels slightly disconnected.

Another model builds a timing system:

  • explicit phase tracking
  • CSS driven by variables
  • JS controlling truth

Now everything aligns:

  • motion
  • sound
  • counting

And the app feels… intentional.

The human version usually does less

But anchors everything in time

Not more animations.

Not more effects.

Just one decision:

Time is the source of truth

Everything else follows.

The part you don’t expect

After building this, you stop seeing:

“a breathing animation”

And start seeing:

“a synchronized system of phases, driven by time, expressed through visuals”

Which is the same pattern behind:

  • audio engines
  • game loops
  • real-time dashboards

Try this before you move on

Don’t just make it smooth.

Make it consistent.

  • Switch presets mid-cycle
  • Tab out and come back
  • Let it run for minutes

Does it stay aligned?

Or does it drift?

If you want to explore how different approaches handle that tension between animation and timing, this is the exact challenge:

👉 https://vibecodearena.ai/share/dabe3e01-ecec-4241-9337-7894057f1d19

Play with it.

But more importantly — observe when it stops feeling natural.

That’s where the real problem is hiding.

Top comments (0)