Lunacy

I joined Icons8 LLC in 2019. After working on Pichon, I became part of the team behind Lunacy, a graphic design application for UI, illustrations, mockups, and other visual work. At that moment the team had around 8 people, mostly developers, and the product itself was about two years old.

It was still a very rough product: written in WPF, available only on Windows, and painfully slow. Originally, Lunacy had started as a .sketch file viewer for Windows, and editing capabilities had been added gradually over time. Rendering was based on Skia, the same graphics engine used by Chrome.

My first work on Lunacy

My first task was to integrate Icons8 icon assets directly into Lunacy. I made all icon sets available inside the product so users could browse them and drag and drop them onto the canvas with minimal friction.

That work also included a substantial licensing and payment integration effort. We supported icon licensing through both the Microsoft Store and the Icons8 website, making the content commercially usable inside the workflow instead of leaving it as a disconnected external asset library.

Performance work and GPU rendering

After that, I focused on performance problems and found a large number of serious issues. Some were fixed quickly, but others took a very long time to solve.

The biggest problem at the time was that everything was rendered on the CPU. On large documents, the application could freeze for several seconds just to render a single frame. I started the long and difficult process of moving rendering to the GPU.

That turned into one of the hardest engineering tasks of my career. WPF works through DirectX 9, while Skia did not simply support the rendering path we needed. Skia could render through OpenGL, but not in a way that directly solved the WPF integration problem. The final architecture required Skia to render through OpenGL, routed via ANGLE, which then rendered into a DirectX 11 texture, which then had to be translated into a DirectX 9 texture for WPF. This also involved SharpDX and OpenTK, and it all had to work without running into the classic WPF airspace problem.

Bringing all of that together into a stable working solution was incredibly difficult. At the time, almost nobody had published a complete practical implementation of this pipeline. I later shared a working public example here: WpfSkiaAngleSharpDxOpenTK.

That was only part of the job. The product also had to become multithreaded. Previously there had been only one thread doing both UI work and canvas rendering. I introduced a dedicated rendering thread and designed a stable, precise, and easy-to-use GL thread dispatcher so we could clearly separate code that had to run on the window thread from code that belonged on the canvas rendering thread.

The result was dramatic: GPU rendering was up to 100 times faster than the old CPU path.

From engineer to team lead

Because of those performance improvements, I was promoted to team lead for Lunacy. In that role I had to rebuild parts of the team: let go of less motivated people, hire stronger ones, and gradually expand the QA team to improve product stability.

I also wrote around 30 internal documents describing our workflows in detail. Those processes remained the foundation of how we worked for years with no major need to replace them. One thing I am proud of is that the team had no destructive internal conflicts. People worked together respectfully, helped each other, and kept the environment professional and friendly.

Even after becoming team lead, scrum master, and product owner, I did not reduce my technical involvement. In Git, out of roughly 50,000 commits across 10 years, about half were mine. The same pattern was visible in Jira. The number of features I personally owned was about three times larger than the combined total owned by the rest of the developers.

I tried to lead by example: showing that difficult tasks could be done quickly, efficiently, and with high quality. A lot of that starts with well-formed tasks, so I held many meetings where I taught people how to write better tickets, decompose work correctly, and reason through difficult implementation questions.

Technical standards and removing legacy

I pushed hard to keep the product free of unnecessary legacy code, to adopt modern technologies, and to actively control technical debt. We frequently upgraded to the newest versions of .NET on release day, or even before release day, and removed dependencies on outdated libraries whenever practical.

One important example was the undo stack. When I joined, it was implemented through closures, which meant that after a crash both the document state and the undo history were lost. It was also extremely difficult to debug.

I redesigned the entire undo system so that it was fully persisted to disk in JSON in a readable format. That gave us two major benefits:

  • after a crash, user actions could be restored
  • the undo system became much easier to debug and unit test

Rewriting Lunacy from WPF to Avalonia

In 2021, I made a major strategic decision: rewrite the application from WPF to AvaloniaUI. We completed that migration in about six months.

As a result, Lunacy began to run on Linux and macOS in addition to Windows. We shipped it through Snapcraft, Flathub, and became the first Avalonia application in the App Store. That last part was especially difficult. Apple Sandbox restrictions were strict enough that I had to involve an Apple employee directly, and together we managed to get the Avalonia application working correctly within the App Store rules.

During that effort I also had to contribute many fixes upstream. I created dozens of pull requests to Avalonia so things like Apple trackpad support, App Store purchases, sandbox file access bookmarks, and other platform-specific behavior would work correctly.

Because those pull requests were not always merged as quickly as we needed, I decided that we should maintain our own forks of Avalonia and related libraries such as AvalonEdit, Avalonia Behaviors, and Avalonia SVG. That required additional work on pipelines and engineering process, but in practice it was absolutely the right decision and did not create a large maintenance burden for us.

I later gave a dotNEXT talk explaining that migration in detail:

Localization, typography, and text editing

Localization was another area I owned. My goal was to make it convenient, compact, and instant to switch, while also keeping memory usage low. I was able to implement that very efficiently. Lunacy supports more than 20 languages.

For Arabic and Hebrew, the UI can switch into proper RTL layout, which looks impressive and is still missing from many desktop applications.

At another point, I also became responsible for typography. I removed the old implementation and rewrote the text system from scratch using Avalonia-based code. That work gave Lunacy a stable text editor with support for RTL languages, styling, and emoji.

The hardest product features

Some of the most difficult parts of Lunacy were Components, Styles, Overrides, and Shared Libraries. I was the person responsible for all of them.

These features were hard not only because of implementation complexity, but because they required keeping a huge number of edge cases and behavioral rules in mind at all times. If you forgot one important detail, something would break immediately, and QA would often catch it only after release if you were careless.

Theme switching and floating UI

One feature I implemented was theme switching. I made it so efficient that even Avalonia developers later asked me how I had achieved 1 ms theme switching, while their own test applications were freezing for about a second. I explained the approach, and they later improved their own switching performance too.

I was also the main driver behind the idea of a floating UI. Making it work was not easy. I had to add support in our Avalonia fork so different pieces of the application's UI could be rendered into separate textures. That way, when the canvas underneath the interface was redrawn, it did not force the whole UI to redraw with it. The result was very fast and very efficient.

Years of rendering optimization

Across all those years, I kept working on rendering. I tried dozens of caching and acceleration strategies. The most effective ones were:

  • removing LINQ from the rendering pipeline
  • moving repeated per-frame work into cacheable precomputed state
  • caching the last frame
  • caching large layers
  • reducing rendering-frame allocations to zero

That zero-allocation rendering path significantly improved performance and, in some cases, allowed us to outperform competing products.

Cloud collaboration

One of the hardest challenges was cloud collaboration. The JSON-based undo stack helped enormously here. Building on top of that, and using CRDT-inspired ideas, we implemented a highly efficient collaboration system.

If you want a good introduction to the CRDT, I recommend this: How to make a real-time collaborative text editor in 5 easy steps! // Rudi Chen

Our goal was to keep server costs as low as possible, so the server side did almost nothing beyond acting as a WebSocket message relay. Documents stayed open on client machines, and users applied incoming changes locally.

That architecture meant that even a very cheap server with 2 CPU cores and 2 GB of RAM could support hundreds of people collaborating in a single document, while also having hundreds of documents open at the same time. We paid very little for that infrastructure.

To make latency practical worldwide, we eventually deployed around 10 servers in different regions and designed the connection architecture so users could collaborate with low ping while still working together across regions without having to manually switch regions inside the application.

Trimming, NativeAOT, and startup speed

Another major challenge was enabling trimming and moving toward NativeAOT. We needed that work to achieve extremely fast startup.

That required removing some dependencies, forking others to eliminate reflection, and doing a large amount of internal cleanup so Lunacy itself would also avoid reflection wherever possible. We also made sure JSON handling could be generated without reflection.

It was a lot of effort, but it paid off: Lunacy starts almost instantly.

Lunacy on the web

Lunacy also became the first large Avalonia application shipped on WASM. The web version of Lunacy took an enormous amount of work.

We had to rewrite many features because the web platform has severe limits in performance, memory, threading and storage. In practice, we had only one thread and no real disk, which forced major architectural changes.

In total, the web version was in development for around 2 to 3 years. We reached the finish line only after overcoming hundreds of serious problems, many of which at different times looked like they might make the whole WASM direction impossible.

Reverse engineering .fig and creating .free

At one point we also had to support .fig files. These are binary files without a public specification, and parsing them required a lot of effort.

I also designed our own design-file format called .free. Its specification is public, and I created it from scratch to solve a number of problems found in both .sketch and .fig. It is more efficient than .sketch and far easier to understand than .fig.

AI, plugins, and privacy

We always tried to stay close to the cutting edge. We were among the early teams to introduce MCP into Lunacy, built a plugin API around those capabilities, and later developed an AI Chat that used modern LLMs to generate very high-quality designs, including layouts with Auto Layout already applied.

I also consistently defended user privacy during product development. At one point there was interest in adding analytics to track user behavior more aggressively, and I was eventually able to persuade leadership that it was the wrong direction.

Today I consider Lunacy one of the most private and secure solutions in graphic design, and that became one of the product's defining characteristics.

Working with users

I read user feedback constantly and made sure that user ideas and reported bugs were represented in our Jira backlog. I often replied to users directly, investigated the root causes of problems, and handled feature improvement requests myself.

I believe working with users should be priority number one for a product owner. We also regularly ran usability interviews with designers who were not familiar with Lunacy, and we used their real behavior to improve the product experience.

Technologies

AvaloniaUI, Skia, NativeAOT, ASP.NET, WebSockets, Postgres, Docker, Entity Framework, C#, Objective-C, WPF, SignalR, OpenGL, OpenTK, SharpDX, AvalonEdit, MVVM, S3, CDN, Linux, GitLab CI, OpenCode, MCP, SVG, CRDT, WASM, GraphQL

Why the project was effectively closed

Why was the project closed in practice? Over all those years, we never had a strong marketing strategy. On the marketing and sales side, we simply lost.

At this point, it is not enough to build a good and honest product. You also need to know how to sell it, and that was our weak side. At some point our founder weighed revenue against costs, looked at the market situation, including the collapse of Figma's stock and the decline in Adobe's stock, and concluded that the project did not have a viable future.

The team was reduced to one part-time person whose role is to keep the product operational.

Final note

I want to say thank you to everyone I worked with at Icons8. You are all real professionals, and it was a pleasure building Lunacy together.