A story about how migrating an interactive office map editor turned into an engineering investigation involving Fabric.js,
tainted canvas, and an architecture that's finally easy to extend.
In most software projects, one sentence usually makes every developer nervous:
"Let's rewrite this module from scratch."
It often means months of development, regression risks, and endless architecture discussions.
Our project was no different.
We develop, a workspace management platform that allows companies to manage office spaces and book desks. One of its core features is an interactive office map editor, where administrators upload floor plans, place desks and meeting rooms, and publish maps for employees.
Over the years, this editor slowly evolved into a real monolith.
And the problem wasn't simply the number of lines of code.
Where It All Started
The editor dated back to the AngularJS era.
The main component had gradually grown into a single file responsible for almost everything:
- loading maps
- working with Fabric.js
- CRUD operations
- keyboard shortcuts
- dialogs
- saving
- event handling
The main editor component alone contained nearly 2,270 lines of code.
Behind it lived another codebase — the map engine itself.
Almost 20,000 lines of TypeScript spread across more than 230 files.
One of the biggest architectural issues was an infinite rendering loop.
fabric.util.requestAnimFrame(() => this.tick());
Even when the user wasn't interacting with the editor, rendering continued forever.
It worked.
But every new feature became more expensive to build.
Why We Decided to Rewrite It
The motivation wasn't AngularJS itself.
The real reason was business requirements.
The product needed completely new capabilities:
- map drafts
- safe publishing
- high-quality printing
- multiple workspace modes
- easier support for new object types
Every new feature pushed harder against the existing architecture.
Eventually it became obvious:
We weren't fighting individual bugs anymore.
We were fighting the architecture itself.
Instead of performing a risky Big Bang migration, we decided to build an entirely new administration module alongside the existing viewer.
Building a New Architecture
We kept Fabric.js.
Everything around it changed.
Instead of one massive controller, we introduced clear responsibilities.
Map Editor
│
▼
Map Engine
│
┌──────┼─────────┐
│ │ │
▼ ▼ ▼
Viewport Registry Changes
Tracker
│
▼
Fabric.js
Each subsystem became responsible for exactly one concern.
- Viewport controls camera movement.
- Object Registry manages map entities.
- Changes Tracker knows only about modifications.
- Map Engine exposes a single API for the editor.
The most pleasant surprise came later.
Adding new functionality no longer required touching half of the application.
Moving the Camera Instead of Moving Objects
One architectural decision simplified much more than we expected.
Instead of physically moving every desk on the canvas, we move the camera using Fabric's viewportTransform.
Objects always stay in their original coordinates.
This dramatically simplified:
- zooming
- panning
- exporting
- printing
Sometimes one architectural decision solves several future problems at once.
The Hardest Problem Wasn't Angular
When the migration started, we believed rewriting nearly 20,000 lines of TypeScript would be the hardest challenge.
It wasn't.
The most difficult part looked completely harmless.
We simply needed to print the office map.
Attempt #1
The obvious solution:
window.print();
Unfortunately, browser printing renders the DOM at roughly screen resolution.
For large office maps, the result was blurry.
Even worse, parts of the editor UI appeared in the printed document.
Not acceptable.
Attempt #2
The next idea looked much better.
Export the Fabric canvas as a high-resolution image.
canvas.toDataURL();
Instead, the browser answered with:
SecurityError:
The canvas has been tainted by cross-origin data.
Why?
The floor plan image was stored on Amazon S3.
Although it rendered perfectly inside the browser, exporting the canvas became impossible because the background image wasn't available with the required CORS configuration.
The browser was doing exactly what it was supposed to do.
Attempt #3
This one finally worked.
Instead of exporting a single PNG, we split printing into two independent layers.
The background image remained untouched.
The Fabric objects were exported separately without the background.
Conceptually, the pipeline became:
Background Image
+
Objects Layer
=
Printable Document
The implementation looked roughly like this:
const objectsDataUrl = exportCanvasRegion({
hideBackground: true,
});
return {
backgroundUrl,
objectsDataUrl,
};
The solution looked more complicated.
In reality, it proved to be much more reliable.
One More Unexpected Problem
After solving the SecurityError, another issue appeared.
Objects slowly drifted away from their correct positions during printing.
The reason turned out to be surprisingly subtle.
The background image was positioned in pixels.
The overlay objects were positioned using percentages.
When the browser recalculated layout for printing, the two layers no longer aligned.
The final fix was surprisingly simple.
We switched background positioning to percentage-based coordinates as well.
Immediately both layers aligned perfectly again.
Problems like this are often the hardest to debug.
Not because the code is wrong.
But because the coordinate model itself is wrong.
What Changed After the Migration
Interestingly, success wasn't measured by fewer lines of code.
The new implementation actually introduced new functionality:
- draft support
- printing
- safe publishing
- multiple workspace modes
- a much cleaner architecture
The number of lines became a meaningless metric.
The important improvement was something else.
Adding new features no longer felt dangerous.
What We Learned
Projects like this remind you that the hardest engineering challenges rarely belong to a particular framework.
Angular evolves.
Fabric.js evolves.
TypeScript evolves.
But the real complexity usually appears somewhere between these technologies.
Sometimes rewriting twenty thousand lines of code isn't the hardest part of the project.
Sometimes it's the seemingly innocent Print button.
That single feature forced us to better understand browser rendering, CORS, Canvas security, coordinate systems, and software architecture than any framework migration ever could.
Final Thoughts
Looking back, the migration wasn't really about Angular.
It was about making the product easier to evolve.
The new architecture didn't dramatically reduce the amount of code.
Instead, it reduced the cost of future changes.
Sometimes that's a far more valuable metric than lines of code.
What's Next?
If this story is interesting, I'd be happy to write a follow-up article explaining how we designed our MapEngine, why we decided to keep Fabric.js, and how we migrated the administration module without stopping product development.
This article is based on our experience building and maintaining an enterprise workspace management platform. Code samples have been simplified, and implementation details have been generalized to respect product confidentiality while preserving the engineering decisions and lessons learned.
Top comments (0)