diff options
author | MetroWind <chris.corsair@gmail.com> | 2025-09-25 16:16:58 -0700 |
---|---|---|
committer | MetroWind <chris.corsair@gmail.com> | 2025-09-25 16:16:58 -0700 |
commit | 5f168246cca42ba54b67bd84fbd69fd6a5b9f820 (patch) | |
tree | ace07dfcb83ea1baac5062a02fe6d0a81ea4bf22 /scripts/main.js | |
parent | a1fa9840be9ca1b092f722be7b61c142cf80d8e8 (diff) |
This repo will be used for all small web apps.
The NMS Freighter Planner is moved into its own dir.
Diffstat (limited to 'scripts/main.js')
-rw-r--r-- | scripts/main.js | 256 |
1 files changed, 0 insertions, 256 deletions
diff --git a/scripts/main.js b/scripts/main.js deleted file mode 100644 index d6b6787..0000000 --- a/scripts/main.js +++ /dev/null | |||
@@ -1,256 +0,0 @@ | |||
1 | const DEFAULT_APP_STATE = { | ||
2 | floor: 0, // This is 0-based. | ||
3 | plan: new Array(FLOOR_COUNT).fill(null).map(() => | ||
4 | new Array(GRID_SIZE_X * GRID_SIZE_Y).fill(0)), | ||
5 | // mouse_state can be “normal”, or “dnd”. | ||
6 | mouse_state: "normal", | ||
7 | dragged_asset_index: null, | ||
8 | plan_code: "", | ||
9 | }; | ||
10 | |||
11 | async function serializePlan(app_state) | ||
12 | { | ||
13 | return compressBytes(Uint8Array.from(app_state.plan.flat())); | ||
14 | } | ||
15 | |||
16 | function genGrid(x_count, y_count, cell_size) | ||
17 | { | ||
18 | let lines = []; | ||
19 | for(let x = 0; x < x_count+1; x++) | ||
20 | { | ||
21 | lines.push(h("line", { | ||
22 | "x1": x * cell_size + 0.5, "x2": x * cell_size + 0.5, "y1": 0.5, | ||
23 | "y2": y_count * cell_size + 0.5, | ||
24 | })); | ||
25 | } | ||
26 | for(let y = 0; y < y_count+1; y++) | ||
27 | { | ||
28 | lines.push(h("line", { | ||
29 | "y1": y * cell_size + 0.5, "y2": y * cell_size + 0.5, "x1": 0.5, | ||
30 | "x2": x_count * cell_size + 0.5, | ||
31 | })); | ||
32 | } | ||
33 | return h("g", {"stroke": "black", "stroke-width": 1}, lines); | ||
34 | } | ||
35 | |||
36 | function AssetView({asset_index, on_drag_begin, on_drag_end}) | ||
37 | { | ||
38 | let bg = ASSET_WHITE_BG; | ||
39 | if(TILES[asset_index].bg === null) | ||
40 | { | ||
41 | bg = null; | ||
42 | } | ||
43 | return h("div", {"class": "Asset", "draggable": true, | ||
44 | "data-asset-index": asset_index, | ||
45 | "ondragstart": on_drag_begin, | ||
46 | "ondragend": on_drag_end, | ||
47 | "title": TILES[asset_index].name,}, | ||
48 | h("svg", ASSET_SVG_PROPS, bg, TILES[asset_index].inner_svg)); | ||
49 | } | ||
50 | |||
51 | function AssetsView({on_drag_begin, on_drag_end}) | ||
52 | { | ||
53 | let views = TILES.map((t, i) => | ||
54 | h(AssetView, {asset_index: i, on_drag_begin: on_drag_begin, | ||
55 | on_drag_end: on_drag_end})); | ||
56 | return h("div", {}, views); | ||
57 | } | ||
58 | |||
59 | function PlanGridView({content, mouse_state, on_drop_asset}) | ||
60 | { | ||
61 | const [mouse_coord, setMouseCoord] = preactHooks.useState(null); | ||
62 | |||
63 | function onDragOver(e) | ||
64 | { | ||
65 | e.preventDefault(); | ||
66 | let b = e.target.getBoundingClientRect(); | ||
67 | let x = e.clientX - b.left; | ||
68 | let y = e.clientY - b.top; | ||
69 | setMouseCoord([x, y]); | ||
70 | } | ||
71 | |||
72 | function onDragLeave(e) | ||
73 | { | ||
74 | setMouseCoord(null); | ||
75 | } | ||
76 | |||
77 | let hilight_box = null; | ||
78 | |||
79 | if(mouse_coord !== null && mouse_state != "dnd") | ||
80 | { | ||
81 | setMouseCoord(null); | ||
82 | } | ||
83 | |||
84 | if(mouse_coord !== null && mouse_state == "dnd") | ||
85 | { | ||
86 | let cell_x = Math.floor(mouse_coord[0] / (CELL_SIZE + 1)); | ||
87 | let cell_y = Math.floor(mouse_coord[1] / (CELL_SIZE + 1)); | ||
88 | hilight_box = h("rect", { | ||
89 | "x": cell_x * (CELL_SIZE + 1) + 0.5, "y": cell_y * (CELL_SIZE + 1) + 0.5, | ||
90 | "width": CELL_SIZE + 1, "height": CELL_SIZE + 1, | ||
91 | "stroke": "#2ed573", "stroke-width": 3, "fill": "transparent"}); | ||
92 | } | ||
93 | let grid_content = content.map((idx, i) => { | ||
94 | if(idx == 0) return null; | ||
95 | let cell_x = i % GRID_SIZE_X; | ||
96 | let cell_y = Math.floor(i / GRID_SIZE_Y); | ||
97 | let bg = ASSET_WHITE_BG; | ||
98 | if(TILES[idx].bg === null) | ||
99 | { | ||
100 | bg = null; | ||
101 | } | ||
102 | return h("g", {"transform": `translate(${cell_x * (CELL_SIZE + 1) + 1} \ | ||
103 | ${cell_y * (CELL_SIZE + 1) + 1})`}, bg, TILES[idx].inner_svg); | ||
104 | }); | ||
105 | |||
106 | let img_width = GRID_SIZE_X * (CELL_SIZE + 1) + 1; | ||
107 | let img_height = GRID_SIZE_Y * (CELL_SIZE + 1) + 1; | ||
108 | return h("svg", {"width": img_width, "height": img_height, | ||
109 | "version": "1.1", "ondragover": onDragOver, | ||
110 | "ondrop": on_drop_asset, | ||
111 | "id": "Grid", "ondragleave": onDragLeave,}, | ||
112 | h("rect", {x: 0, y: 0, width: img_width, height: img_height, | ||
113 | fill: "#c0c0c0", "stroke-width": 0}), | ||
114 | grid_content, genGrid(GRID_SIZE_X, GRID_SIZE_Y, CELL_SIZE + 1), | ||
115 | hilight_box); | ||
116 | } | ||
117 | |||
118 | function PlanView({plan, mouse_state, on_drop_asset}) | ||
119 | { | ||
120 | return h("div", {"id": "PlanView"}, | ||
121 | h("div", {"id": "EntryIndicator"}, "⬇️"), | ||
122 | h(PlanGridView, {content: plan, mouse_state: mouse_state, | ||
123 | on_drop_asset: on_drop_asset})); | ||
124 | } | ||
125 | |||
126 | // on_floor_change takes one argument, which is the 0-based floor number. | ||
127 | function FloorSelector({floor, on_floor_change}) | ||
128 | { | ||
129 | return h("div", {"id": "FloorSelectorWrapper", "class": "ButtonRowLeft"}, | ||
130 | h("label", {}, "Floor"), | ||
131 | h("input", {"id": "InputFloor", "type": "number", "step": 1, "min": 1, | ||
132 | "max": 15, "value": floor, | ||
133 | onchange: e => on_floor_change(e.target.value - 1),},)); | ||
134 | } | ||
135 | |||
136 | function App({initial_state}) | ||
137 | { | ||
138 | const [state, setState] = preactHooks.useState(initial_state); | ||
139 | |||
140 | // This event is targeted at the dragged asset. | ||
141 | function onDragAssetBegin(e) | ||
142 | { | ||
143 | let new_state = structuredClone(state); | ||
144 | new_state.mouse_state = "dnd"; | ||
145 | new_state.dragged_asset_index = | ||
146 | parseInt(e.target.getAttribute("data-asset-index")); | ||
147 | setState(new_state); | ||
148 | } | ||
149 | |||
150 | // This event is targeted at the grid. | ||
151 | function onDropAssetOnGrid(e) | ||
152 | { | ||
153 | if(e.dragEffect === "none") return; | ||
154 | let grid = document.getElementById("Grid"); | ||
155 | let b = grid.getBoundingClientRect(); | ||
156 | let x = e.clientX - b.left; | ||
157 | let y = e.clientY - b.top; | ||
158 | let cell_x = Math.floor(x / (CELL_SIZE + 1)); | ||
159 | let cell_y = Math.floor(y / (CELL_SIZE + 1)); | ||
160 | let cell_index = cell_y * GRID_SIZE_X + cell_x; | ||
161 | let new_state = structuredClone(state); | ||
162 | new_state.plan[new_state.floor][cell_index] = state.dragged_asset_index; | ||
163 | new_state.mouse_state = "normal"; | ||
164 | new_state.dragged_asset_index = null; | ||
165 | serializePlan(new_state).then((s) => { | ||
166 | new_state.plan_code = s; | ||
167 | setState(new_state); | ||
168 | const url = new URL(location); | ||
169 | url.searchParams.set("plan", s); | ||
170 | window.history.pushState({}, "", url); | ||
171 | }); | ||
172 | } | ||
173 | |||
174 | function onClickSaveImg() | ||
175 | { | ||
176 | let grid_width = GRID_SIZE_X * (CELL_SIZE + 1) + 1; | ||
177 | let grid_height = GRID_SIZE_Y * (CELL_SIZE + 1) + 1; | ||
178 | let canvas = document.createElement('canvas'); | ||
179 | canvas.width = grid_width; | ||
180 | canvas.height = grid_height; | ||
181 | let context = canvas.getContext("2d"); | ||
182 | var svg_str = new XMLSerializer().serializeToString( | ||
183 | document.getElementById("Grid")); | ||
184 | var img = new Image(); | ||
185 | img.onload = function() | ||
186 | { | ||
187 | context.drawImage(this, 0, 0); | ||
188 | // Open PNG image in new tab. | ||
189 | var dataURL = canvas.toDataURL("image/png"); | ||
190 | var new_tab = window.open('about:blank', 'image from canvas'); | ||
191 | new_tab.document.write("<img src='" + dataURL + "' alt='from canvas'/>"); | ||
192 | } | ||
193 | img.src = 'data:image/svg+xml; charset=utf8, ' + | ||
194 | encodeURIComponent(svg_str); | ||
195 | } | ||
196 | |||
197 | // new_floor is 0-based. | ||
198 | function onFloorChange(new_floor) | ||
199 | { | ||
200 | let new_state = structuredClone(state); | ||
201 | new_state.floor = new_floor; | ||
202 | setState(new_state); | ||
203 | } | ||
204 | |||
205 | return h("div", {}, | ||
206 | h("div", {"class": "MainWrapper"}, | ||
207 | h("aside", {}, | ||
208 | h(FloorSelector, {floor: state.floor + 1, | ||
209 | on_floor_change: onFloorChange}), | ||
210 | h("div", {"class": "LabeledPanel"}, | ||
211 | h("h2", {}, "Rooms"), | ||
212 | h(AssetsView, {on_drag_begin: onDragAssetBegin,}))), | ||
213 | h(PlanView, {plan: state.plan[state.floor], | ||
214 | mouse_state: state.mouse_state, | ||
215 | on_drop_asset: onDropAssetOnGrid,})), | ||
216 | h("hr", {}), | ||
217 | h("div", {}, | ||
218 | h("textarea", {readonly: true}, state.plan_code)), | ||
219 | h("div", {"class": "ButtonRow"}, | ||
220 | h("button", {onclick: onClickSaveImg, type: "button"}, | ||
221 | "Open as Image!")), | ||
222 | ); | ||
223 | } | ||
224 | |||
225 | function render(app_state) | ||
226 | { | ||
227 | preact.render(h(App, {initial_state: app_state}), | ||
228 | document.getElementById("Plan")); | ||
229 | } | ||
230 | |||
231 | const paramsString = window.location.search; | ||
232 | const searchParams = new URLSearchParams(paramsString); | ||
233 | const encoded_plan = searchParams.get("plan"); | ||
234 | if(encoded_plan === null) | ||
235 | { | ||
236 | render(DEFAULT_APP_STATE); | ||
237 | } | ||
238 | else | ||
239 | { | ||
240 | decompressBytes(encoded_plan).then(a => { | ||
241 | let state = structuredClone(DEFAULT_APP_STATE); | ||
242 | let concated_plan = Array.from(a); | ||
243 | console.debug(concated_plan); | ||
244 | state.plan = []; | ||
245 | for(let i = 0; i < GRID_SIZE_X * GRID_SIZE_Y * FLOOR_COUNT; | ||
246 | i += GRID_SIZE_X * GRID_SIZE_Y) | ||
247 | { | ||
248 | let floor_slice = | ||
249 | concated_plan.slice(i, i + GRID_SIZE_X * GRID_SIZE_Y); | ||
250 | console.debug(floor_slice); | ||
251 | state.plan.push(floor_slice); | ||
252 | } | ||
253 | state.plan_code = encoded_plan; | ||
254 | render(state); | ||
255 | }); | ||
256 | } | ||