Map Maker Online: Creating a Map Editing Software
Start of the next software development journey
Hi, this day marks the announcement of my new in-browser editing application. I've been working on it for the past two weeks. We will look at the idea, execution, and progress, as well as plans for the future,
So let's dive in!
· Inspiration
· Goal Of My Project
· Getting Started:
· Tech Stack
∘ React.js:
∘ Next.js:
∘ React/Signals:
∘ MUI Library
∘ TypeScript:
∘ Node.js:
· Setting Up Development Environment
· Challenges and Solutions
∘ Passing Information Around
∘ Switching Canvases:
∘ Implementing state
∘ Debugging:
· Current Features
∘ Drawing
∘ Background Layer
∘ Marker Layer:
∘ Frontline Layer:
· Live Demo
· Your Feedback Matters
· Conclusion
· About the Author
Inspiration
Some months ago a YouTube history creator called Eastory, who focuses on creating infographic maps, published a video titled Eastory 400 000 Subs Q&A. In the video, he showcased some of the tools and work that go into them. The whole development process seemed needlessly complicated featuring multiple software and hundreds of hours of work. That is why I decided to get my take on creating an app that would simplify the production of animated maps.
Goal Of My Project
The aim is to create an editing software, that would be able to combine the key features of Blender with GIMP and create a fast-paced development experience right in the browser
The app will be composed of multiple layers of canvases with their own settings and functionality and simple transition. They will be compiled into frames on a timeline. The files from the timeline could be then compiled into a single file to create an animation.
I am planning to put demo versions of the projects online during development, so you can test out the newest features.
Getting Started:
Tech Stack
Aiming the development on the browser allows me to use the following technology:
React.js:
- React is a declarative, efficient, and flexible JavaScript library for building user interfaces. It allows developers to create reusable UI components and manage the state of an application efficiently. It is the foundation of frontend development, providing a powerful and efficient way to build interactive and dynamic user interfaces, especially when combined with Next.js.
Next.js:
- Next.js is a React framework that simplifies the development of server-side rendered (SSR) and statically generated web applications. It offers automatic code splitting, a fast refresh feature for development, and a strong convention for file-based routing. Additionally, Next.js provides excellent performance optimization out of the box. I have chosen it for its seamless integration with React, efficient server-side rendering capabilities, and structured approach to building complex web applications with ease.
React/Signals:
- React Signals is a state management library for React applications. It facilitates a unidirectional data flow and simplifies the handling of state changes in React components. It is much more intuitive than React hooks which are the standard when working inside the React framework.
MUI Library
- Excellent library for creating a unified design across the whole project. Its main features are pre-styled HTML components, the creation of custom themes, and seamless integration with popular JavaScript frameworks.
TypeScript:
- TypeScript is a superset of JavaScript that adds static typing to the language. It helps catch common errors during development, improves code readability, and provides better tooling support. Inside the React environment, it works even better, thanks to the lack of need to run additional cmd commands for compiling files.
Node.js:
- Node.js is a JavaScript runtime built on Chrome’s V8 JavaScript engine. It allows server-side execution of JavaScript, enabling the development of scalable and high-performance server applications. It will serve as the backend runtime for my application, providing a non-blocking, event-driven architecture that aligns well with JavaScript.
Setting Up Development Environment
First I have created the skeleton of my app using the Next.js initialization command.
npx create-next-app@latest mapping-project
Then I proceeded to create the folders:
app
┣ canvasEditor
┃ ┣ layers
┃ ┣ settings
┃ ┣ theme
┃ ┣ Globals.tsx
┃ ┣ page.tsx
┣ components
┃ ┣ drawing
┃ ┣ frontline
┃ ┣ markerLayer
┃ ┣ settings
┃ ┗ utility
types
┣ GeometryTypes.tsx
┗ OtherTypes.tsx
The structure might change with the growth of the application, but for now it works fine.
After that, I installed the libraries and was ready to write some code!
{
"name": "mapping-software",
"version": "0.1.0",
"private": true,
"configurations": [
{
"name": "Next: Chrome",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}",
"sourceMapPathOverrides": {
"webpack:///./*": "${webRoot}/*"
}
}
],
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.14.17",
"@preact/signals": "^1.2.1",
"html2canvas": "^1.4.1",
"next": "14.0.0",
"react": "^18",
"react-dom": "^18",
"react-dropzone": "^14.2.3",
"uuidv4": "^6.2.13"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "14.0.0",
"postcss": "^8",
"postcss-preset-env": "^9.2.0",
"tailwindcss": "^3.3.5",
"typescript": "^5"
}
}
Challenges and Solutions
Passing Information Around
I am using both React built-in useContext hook as well as Preact Signals. The first is useful when handling variables that change quickly, since I have found, that changing preact signals contains a small delay. On the other hand, signals can simplify exporting of value, so I use them whenever their main disadvantage isn't a problem
export const CanvasProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const canvasRef = useRef<HTMLCanvasElement | undefined>(null);
const markerCanvasRef = useRef<HTMLCanvasElement | null>(null);
const frontlineCanvasRef = useRef<HTMLCanvasElement | null>(null);
const compiledCanvasRef = useRef<HTMLCanvasElement | null>(null);
const [frontLines, setFrontlines] = useState<FrontlineData[]>([])
const backgroundCanvasRef = useRef<HTMLCanvasElement | null>(null);
const [globalData, setGlobalData] = useState<GlobalDataType>({ mouseDownTime: 0 });
const canvasContextValue: CanvasContextType = {
canvasRef,
markerCanvasRef,
frontlineCanvasRef,
backgroundCanvasRef,
compiledCanvasRef,
frontLines,
setFrontlines
};
const updateGlobalData:UpdateGlobalDataType = (paramName , paramValue ) => {
setGlobalData((prevData) => ({
...prevData,
[paramName]: paramValue,
}));
};
const globalDataContextValue: GlobalDataContextType = {
GlobalData: globalData,
updateGlobalData,
};
return (
<CanvasContext.Provider value={canvasContextValue}>
<GlobalDataContext.Provider value={globalDataContextValue}>
{children}
</GlobalDataContext.Provider>
</CanvasContext.Provider>
);
};
export const settings: Settings = signal({
radius: 5,
color: `#000000`,
lineType: "squared",
activeLayer: "draw",
canvasSize: { x: 800, y: 600 },
markerSettings: {
width: 40,
color: `#000000`,
textColor: `#ffffff`,
topValue: "X",
bottomValue: "Y",
imageURL: null,
popularMarkerColors: [],
},
popularColors: [],
canvasZindexes: { marker: 10, draw: 10, background: 0, frontLine: 10, compiled:0 },
});
export const frontLineSettings: FrontLineSettings = signal({
insertionPointIndex: -1,
frontLineColor: "#0000ff",
activeFrontLine: null,
frontLines: [],
controlPointRadius: 5
});
export const markers:MarkerArraySignal = signal([])
export const backgroundImage = signal<File | null>(null);
Switching Canvases:
The app has different editing layers for editing maps:
- drawing layer(basically ms paint)
- frontline editing layer(for easily creating and moving lines)
- unit marker layer(for placing units on the map)
- background-image layer(to set the background map)
- compiled layer(to see the final look of the canvas)
All layers have a different default zIndex which is increased when I make the layer active using this selection panel
All the other layers are then moved to the background and their opacity decreases to 40%.
Implementing state
I have found out that there is a useful React hook called reducer, that can be used as a state machine handler. A state machine is basically a code structure where your algorithm uses a different set of commands based on the value of a certain variable. I have already talked more about them in this article.
const reducer: React.Reducer<DrawingState, DrawAction> = (state, action) => {
// console.log("SWITCHING TO ", action.type, action?.payload);
switch (action.type) {
case "DRAW":
const drawPayload = action.payload as DrawPayload;
console.log(drawPayload);
drawPayload.drawFunction(
drawPayload.drawArgs?.ctx,
drawPayload.drawArgs.x,
drawPayload.drawArgs.y,
drawPayload.drawArgs.radius,
drawPayload.drawArgs.color
);
return DrawingState.Drawing;
case "ERASE":
const erasePayload = action.payload as ErasePayload;
erasePayload.eraseFunction(erasePayload.eraseArgs);
return DrawingState.Erasing;
case "MOUSE_UP":
case "MOUSE_LEAVE":
return DrawingState.Idle;
case "ENTER_BUCKET_MODE":
return DrawingState.BucketFill === state
? DrawingState.Idle
: DrawingState.BucketFill;
default:
console.error("INVALID ACTION: ");
return state;
}
};
**State machines for my drawing layer
Debugging:
A big problem with debugging in React is, that the page constantly refreshes. When you want to print a variable in a component like this, it will spam your console with messages.
import React from 'react'
const foo = () => {
console.log('important info')
return (
<div>foo</div>
)
}
export default foo
To counter that I have created a debugging window, that shows the necessary information right in the app.
import React from 'react';
type DebugInfoProps = {
data: Record<string, any>;
};
const DebugInfo: React.FC<DebugInfoProps> = ({ data }) => (
<div style={{ position: 'fixed', top: '10px', right: '10px', padding: '10px', background: 'rgba(255, 255, 255, 0.7)', zIndex:"1000" }}>
<p>Debug Information:</p>
{Object.entries(data).map(([key, value]) => (
<p key={key}>{`${key}: ${JSON.stringify(value)}`}</p>
))}
</div>
);
export default DebugInfo;
Then I mounted it on the page.tsx and added the information I wanted to view
return (
< >
<DebugInfo
data={{
radius: settings.value.radius,
// canvasState: canvasState,
color: settings.value.color,
layer: settings.value.activeLayer,
mousePosition: mousePosition,
mousDownTime: elapsedTime,
numMarkers:markers.value.length
// activeFrontLine: frontLineSettings.value.activeFrontline?.idNum,
// GlobalData: GlobalData.mouseDownTime,
}}
/>
<CanvasSettings />
<DrawingCanvas />
<Timeline />
</>
);
};
So now everything shows inside of a rectangle in the top right corner.
Current Features
Drawing
In the drawing layer, you can use basic paint settings like
- pencil width
- choosing different line shapes
- eraser
- changing color
- defining favorite colors
In the future, I want to add other common tools like bucket fill, and ruler, or draw more geometrical shapes like triangles.
Background Layer
This layer allows you to choose the background image (preferably a map) to draw on. Because it is on a different layer, you can draw whatever you want on it without mutilating it.
I am planning to add these features:
- resizing of background images
- basic editing of background images
Marker Layer:
This layer is dedicated to adding units, you can place them wherever you want, move them wherever you want, and customize their individual appearance through the settings.
Currently, you can change their color, text, and background image.
Frontline Layer:
The frontline layer allows you to define a line on the map by placing points and then moving them around to the desired shape. You are even able to create cyclic lines as well as changing their color and width
Live Demo
To check out the current state, check out this webpage > Live Demo
Your Feedback Matters
I would be extremely grateful for any feedback possible. If you have any feature that you would like to see in the app you can do it here.
If you want to watch the development more closely you can check out my new discord here > Map Maker Online
Conclusion
This is the current state of the project! We have looked at the setup of the project environment, the architecture, and the first features of the editor. These are some of the new features you can look forward to:
- Curved Lines
- Bucket Fill
- Quick Erase
- Switching between frames
- and more…
If you are interested I once again suggest joining my discord server and sharing your opinion and feedback. I hope I will see you in the next diary entry!
About the Author
My name is Andrew. I have been coding for the past 2 years. I am primarily focused on coding in JavaScript/react with some knowledge of Python and GD script for game development. I have already finished projects like a fullstack web app for my school magazine a chess clone or a top-down randomly generated game. In the future, I want to create easy-to-use web applications utilizing the power of AI and prompt engineering.