Devlog 3 - Creating a Vibrant Sky


Harmonies Garden is a minimalist game, exclusively composed of a UI on a grid. But that doesn't mean it shouldn't be pretty! In fact, I wanted its visuals to emphasis the musical experience, and have a background that is animated with the music. Nothing too detailed (it's a background after all), but at least some kind abstract visuals that would evolve with time and gain intensity along with the music. So I decided to be a bit ambitious, and made a shader for it.

Shader, the easy way

I'm far from being an expert in shaders, but I still wrote some for my previous game. I made them with Unity Shader Graph, a tool allowing to create shaders with a visual editor. I've also used Blender's node editor to make some procedural textures, like my itch page background. So I assumed that maybe I was ready to play in the grown up field now and write shaders directly in GLSL? Well, after some attempts it quickly turned out to be unsuccessful. Reading examples was hard, it was too abstract for me to visualize the possibilities. And when trying to write, I felt blocked, I didn't know which tool to use, what possibilities could be explored, and wasn't able to iterate on ideas. I couldn't get creative this way.

So eventually I searched for a shader visual editor similar to Unity Shader Graph. Surely there should be some good options on the web? And indeed they were! I stumbled upon NodeToy, a graph editor for shader that was just what I was looking for! Just like Unity Shader Graph, I became more expressive with it. I could see what each step was doing, experiment with the tools, adjust values and see the result in real time... This way of creating shader is really more intuitive and satisfying! And it's not just because it's no-code (I'm a developer, after all). I think being able to see the composition of a shader in a non-linear way helps to understand it. I can isolate functions, directly see where a value is used just by following threads, or conversely understand what are its input... It's kinda like using a DAW or a video editor, you get the feeling of putting your hands directly in the thing you're making. It should also be mentioned that NodeToy's UX is really great. Clean, intuitive, and most importantly playful.


But NodeToy has a second powerful feature: its Community Section. Just like ShaderToy, it is filled with creations from other users. You can immediately see their graph, change it, or even fork it. It's the perfect way to learn. It allows to see the "inside" of a shader, and understand it better than with its raw code or tutorial. Because once again, you can explore it freely, see what each operation is doing and preview their result, and interact directly with it. It is really satisfying to explore the creations in NodeToy and learn new tricks in most of them. For example, the implementation of the stars in Harmonies Garden's sky is directly inspired by a this stars shader. A Voronoi noise passed through a step and another Perlin noise can do wonders!


For now Nodetoy has a free pricing option. Its biggest contraint is that your creations can only be public. Which is fine by me (you can take a look at my sky shader if you're curious). I guess it's still a young product, and thus need to build a user base. This economic model is an ideal one to make users share a lot of creations and show what the tool is capable of. I really hope that it will evolve in the right way and won't fall into abusive pricing models once it becomes popular! If this sounds like a sponsored post, know that it's not. I'm just really pleased by this software, it's a great one, and those are rare. So it deserves to be applauded!

Exporting and reimporting

ShaderToy allows to export a shader into a GLSL script. It's generated code, so hardly readable, but that's not really an issue here. For using it in a web app, I only need to make some small adjustments (mostly in the headers). The question now is: how to use it in a Svelte app?

Welp, turns out that there is a lib for that! Svader provides a component that takes a shader and uniforms as parameters, and display it on a canvas. It's absurdly simple to use, and just matches exactly what I needed. So this is the second not-sponsored-recommandation of this post. Just look at how the component code for the sky looks:

<script lang="ts>
  import shaderCode from './shader.frag?raw';
  import { WebGlShader } from 'svader';
  
  const DEFORM_SCALE = 0.6;
  const DEFORM_SPEED = -0.05;
  const CLOUD_SCALE = 2.8;
  const CLOUD_BRIGHT_MIN = 0.7;
  const CLOUD_BRIGHT_MAX = 1.1;
  // etc
  
  const { color, cloudIntensity = 0, starIntensity = 0 }: SkyBackgroundProps = $props();
  const cloudBrightness = $derived(lerp(CLOUD_BRIGHT_MIN, CLOUD_BRIGHT_MAX, cloudIntensity));
  const starBrightness = $derived(lerp(STAR_BRIGHTNESS_MIN, STAR_BRIGHTNESS_MAX, starIntensity));
  const starSize = $derived(lerp(STAR_SIZE_MIN, STAR_SIZE_MAX, starIntensity));
  // ... among other things ...
</script>
  
<div class="sky-background">
  <WebGlShader
    code={shaderCode}
    parameters={[
      { name: 'u_time', value: 'time' },
      { name: 'u_resolution', value: 'resolution' },
      { name: 'cloud_scale', type: 'float', value: CLOUD_SCALE },
      { name: 'cloud_bright', type: 'float', value: cloudBrightness },
      { name: 'star_size', type: 'float', value: starSize },
      { name: 'star_brightness', type: 'float', value: starBrightness }
  />
</div>

Easy, right? As you might have guessed, cloudIntensity and starInstensity are obtained from the music's audio data. See my previous devlog to learn more about it.

I also omitted the color part, because it is a bit too verbose. I just have a list of color tuples (dark and bright), then I select the one corresponding to the current field. They are passed to the WegGlShader as parameters with the vec4 type. This mean that I have to write them as an array of 4 floats (the last one being for alpha, so always 1).

type ColorVector = [number, number, number, number];
const BEIGE_1: ColorVector = [0.17647058823529413, 0.1411764705882353, 0.09019607843137255, 1];
const BEIGE_2: ColorVector = [0.5254901960784314, 0.44313725490196076, 0.3176470588235294, 1];
const BLUE_1: ColorVector = [0.07058823529411765, 0.13333333333333333, 0.19215686274509805, 1];
const BLUE_2: ColorVector = [0.17254901960784313, 0.30980392156862746, 0.47843137254901963, 1];

I obtained those values with an utility function that convert hexadecimal values of colors (that I already have in the CSS) into number arrays. Technically, I could have make a script that dynamically read colors from the style and convert it. But this would have taken more time to write than it would save me. Sometime dirty solutions are the most optimal ones.

So there you have it. Writing a shader and implementing it as a background in a Svelte App was way less complex than I anticipated! It actually took me only a week to complete it (a working week even, I did this on spare time). All of this because I found just the right tools. Isn't it nice when things go so well?


Backup plan

There is one last thing that I should mention. What happen if the player's browser doesn't support WebGL? This case is pretty rare now, but still plausible, and I wouldn't want them to experience a black background! I still wanted those players to have a colored page, ideally with a gradient to not make it dull, and still animated with music. Is this feasible in CSS?

As it turns out, yes. Thanks to a CSS feature I didn't know: @property. This allows to create CSS variables in a scope, and treat them as CSS properties. Meaning that you can animate them or make them transition!

In this solution, I use 3 variables: --bg-gradient-dark, --bg-gradient-light, and --bg-gradient-pos. The first two are the colors that make the gradient. They are changed for each field, blending between each other during the transition. As for --bg-gradient-pos, it's a value between 80 and 110 that is moved with the music's volume.

@property --bg-gradient-light {
  syntax: '<color>';
  initial-value: white;
  inherits: false;
}
@property --bg-gradient-dark {
  syntax: '<color>';
  initial-value: black;
  inherits: false;
}
  
.dynamic-background {
  transition:
    background-color 0.2s,
    --bg-gradient-light 0.2s,
    --bg-gradient-dark 0.2s;
  background: radial-gradient(
    at 50% 100%,
    var(--bg-gradient-light) 0%,
    var(--bg-gradient-dark) var(--gradient-pos)
  );
  
  &.beige {
    --bg-gradient-light: var(--beige-2);
    --bg-gradient-dark: var(--beige-0);
  }
  &.blue {
    --bg-gradient-light: var(--blue-2);
    --bg-gradient-dark: var(--blue-0);
  }
}

Now even when WebGL disabled, players still have an animated background. I might add a static stars image at some point.


You might have noticed that there is a lot of movement in the page now. Small animations are everywhere! This poses an accessibility issue for users that are too easily distracted by visual noises. So one of the feature of the Beta will be an option to disable these animation to have a static background instead (it should also initialize itself with the browser's preferences).

I hope these last two devlog were insightful. I'm really proud of the results. Harmonies Garden finally looks like a game with an identity. Speaking of which, I have a confession to make: Harmonies Garden isn't actually the final name of the project. As you can see with the stars in the background, I'd like to give it some kind of astronomy vibes. As the game enters its Beta stage, it is time I settle on its definitive name. And you'll discover it very soon!

Leave a comment

Log in with itch.io to leave a comment.