diff options
Diffstat (limited to 'button-maker/scripts')
-rw-r--r-- | button-maker/scripts/lib.js | 79 | ||||
-rw-r--r-- | button-maker/scripts/main.js | 279 | ||||
-rw-r--r-- | button-maker/scripts/tiles.js | 94 |
3 files changed, 452 insertions, 0 deletions
diff --git a/button-maker/scripts/lib.js b/button-maker/scripts/lib.js new file mode 100644 index 0000000..372d9d7 --- /dev/null +++ b/button-maker/scripts/lib.js | |||
@@ -0,0 +1,79 @@ | |||
1 | // In Firefox (on Mac only?), for <input type="color">, setting value | ||
2 | // to abbreviated hex form (e.g. #123) doesn’t work. So all colors | ||
3 | // should be specified in full form. | ||
4 | const DEFAULT_NETSCAPE_PARAMS = { | ||
5 | color_bg: "#c0c0c0", | ||
6 | logo_url: "https://upload.wikimedia.org/wikipedia/commons/0/08/Netscape_icon.svg", | ||
7 | text_top: { | ||
8 | content: "Netscape", | ||
9 | pos: [31, 11], | ||
10 | font_size: 10.5, | ||
11 | color: "#000000", | ||
12 | }, | ||
13 | text_bottom: { | ||
14 | content: "Now!", | ||
15 | pos: [30, 28], | ||
16 | font_size: 17.5, | ||
17 | color: "#ff0000", | ||
18 | }, | ||
19 | banner: { | ||
20 | style: "corner_bottom_right", // corner_bottom_right or right | ||
21 | color: "#008080", | ||
22 | text: { | ||
23 | content: "5.0", | ||
24 | pos: [65, 46], | ||
25 | font_size: 8, | ||
26 | color: "#ffffff", | ||
27 | }, | ||
28 | }, | ||
29 | }; | ||
30 | |||
31 | const DEFAULT_APP_STATE = { | ||
32 | button_type: "netscape", | ||
33 | button_params: DEFAULT_NETSCAPE_PARAMS, | ||
34 | } | ||
35 | |||
36 | const SVG_ATTRIBUTES = {viewBox: "0 0 88 31", width: 88, height: 31}; | ||
37 | |||
38 | // Return three integers as an array. | ||
39 | function parseHexColor(color_str) | ||
40 | { | ||
41 | console.debug("Parsing", color_str); | ||
42 | m = color_str.match(/^#([0-9a-f]{3})$/i); | ||
43 | if(m) | ||
44 | { | ||
45 | // in three-character format, each value is multiplied by 0x11 to give an | ||
46 | // even scale from 0x00 to 0xff | ||
47 | return [ | ||
48 | parseInt(m[1].charAt(0),16)*0x11, | ||
49 | parseInt(m[1].charAt(1),16)*0x11, | ||
50 | parseInt(m[1].charAt(2),16)*0x11 | ||
51 | ]; | ||
52 | } | ||
53 | |||
54 | m = color_str.match(/^#([0-9a-f]{6})$/i); | ||
55 | if(m) | ||
56 | { | ||
57 | return [ | ||
58 | parseInt(m[1].substr(0,2),16), | ||
59 | parseInt(m[1].substr(2,2),16), | ||
60 | parseInt(m[1].substr(4,2),16) | ||
61 | ]; | ||
62 | } | ||
63 | return null; | ||
64 | } | ||
65 | |||
66 | function lighten(rgb, delta) | ||
67 | { | ||
68 | return [rgb[0] + delta, rgb[1] + delta, rgb[2] + delta]; | ||
69 | } | ||
70 | |||
71 | function darken(rgb, delta) | ||
72 | { | ||
73 | return [rgb[0] - delta, rgb[1] - delta, rgb[2] - delta]; | ||
74 | } | ||
75 | |||
76 | function color2Str(color) | ||
77 | { | ||
78 | return `rgb(${color[0]} ${color[1]} ${color[2]})`; | ||
79 | } | ||
diff --git a/button-maker/scripts/main.js b/button-maker/scripts/main.js new file mode 100644 index 0000000..7232064 --- /dev/null +++ b/button-maker/scripts/main.js | |||
@@ -0,0 +1,279 @@ | |||
1 | const h = preact.h; | ||
2 | |||
3 | function ButtonViewNetscape({app_state}) | ||
4 | { | ||
5 | const params = app_state.button_params; | ||
6 | console.debug("Rendering netscape params", params); | ||
7 | let color_bg = parseHexColor(params.color_bg); | ||
8 | let color_hilight_1 = color2Str(lighten(color_bg, 63)); | ||
9 | let color_hilight_2 = color2Str(lighten(color_bg, 31)); | ||
10 | let color_shadow_1 = color2Str(darken(color_bg, 0xc0 - 0xa)); | ||
11 | let color_shadow_2 = color2Str(darken(color_bg, 0xc0 - 0x54)); | ||
12 | |||
13 | let ribbon = null; | ||
14 | let ribbon_text = null; | ||
15 | if(app_state.button_params.banner.style == "corner_bottom_right") | ||
16 | { | ||
17 | ribbon = h("polygon", {points: "86,8 86,24 81,29 65,29", | ||
18 | fill: params.banner.color}); | ||
19 | ribbon_text = h("text", { | ||
20 | x: params.banner.text.pos[0], y: params.banner.text.pos[1], | ||
21 | "font-family": "sans-serif", | ||
22 | "dominant-baseline": "middle", "text-anchor": "middle", | ||
23 | "transform-origin": "center", "transform":"rotate(-45)", | ||
24 | "font-weight": "bold", "font-size": params.banner.text.font_size, | ||
25 | fill: params.banner.text.color, "stroke-width": 0}, | ||
26 | params.banner.text.content); | ||
27 | } | ||
28 | else if(app_state.button_params.banner.style == "right") | ||
29 | { | ||
30 | ribbon = h("rect", {x: 78, y: 2, width: 8, height: 27, | ||
31 | fill: app_state.button_params.banner.color}); | ||
32 | ribbon_text = h("text", { | ||
33 | x: params.banner.text.pos[0], y: params.banner.text.pos[1], | ||
34 | "font-family": "sans-serif", "dominant-baseline": "middle", | ||
35 | "text-anchor": "middle", "transform-origin": "center", | ||
36 | transform: "rotate(-90)", "font-weight": "bold", | ||
37 | "font-size": params.banner.text.font_size, | ||
38 | fill: params.banner.text.color, "stroke-width": 0}, | ||
39 | params.banner.text.content); | ||
40 | } | ||
41 | |||
42 | return h("svg", SVG_ATTRIBUTES, | ||
43 | h("g", {"stroke-width": 0}, | ||
44 | h("polygon", {points: "0,0 88,0 87,1 1,1", | ||
45 | fill: color_hilight_1}), | ||
46 | h("polygon", {points: "1,1 87,1 86,2 2,2", | ||
47 | fill: color_hilight_2}), | ||
48 | h("polygon", {points: "88,0 88,31 87,30 87,1", | ||
49 | fill: color_shadow_1}), | ||
50 | h("polygon", {points: "87,1 87,30 86,29 86,2", | ||
51 | fill: color_shadow_2}), | ||
52 | h("polygon", {points: "88,31 0,31 1,30 87,30", | ||
53 | fill: color_shadow_1}), | ||
54 | h("polygon", {points: "87,30 1,30 2,29,86,29", | ||
55 | fill: color_shadow_2}), | ||
56 | h("polygon", {points: "0,0 1,1 1,30 0,31", | ||
57 | fill: color_hilight_1}), | ||
58 | h("polygon", {points: "1,1 2,2 2,29 1,30", | ||
59 | fill: color_hilight_2}), | ||
60 | h("rect", {x: 2, y: 2, width: 84, height: 27, | ||
61 | fill: params.color_bg}), | ||
62 | ribbon), | ||
63 | h("image", {href: params.logo_url, x: 3, y: 3, width: 25, | ||
64 | height: 25}), | ||
65 | h("text", {x: params.text_bottom.pos[0], | ||
66 | y: params.text_bottom.pos[1], "font-family": "Kalam", | ||
67 | "font-size": params.text_bottom.font_size, | ||
68 | fill: params.text_bottom.color, "stroke-width": 0}, | ||
69 | "Now!"), | ||
70 | h("text", {x: params.text_top.pos[0], y: params.text_top.pos[1], | ||
71 | "font-family": "sans-serif", | ||
72 | "font-size": params.text_top.font_size, | ||
73 | fill: params.text_top.color, "stroke-width": 0}, | ||
74 | params.text_top.content), | ||
75 | ribbon_text); | ||
76 | } | ||
77 | |||
78 | function TextControlView({params, on_change}) | ||
79 | { | ||
80 | console.debug("Rendering text params", params); | ||
81 | function onContentChange(e) | ||
82 | { | ||
83 | let new_params = structuredClone(params); | ||
84 | new_params.content = e.target.value; | ||
85 | on_change(new_params); | ||
86 | } | ||
87 | |||
88 | function onPosXChange(e) | ||
89 | { | ||
90 | let new_params = structuredClone(params); | ||
91 | new_params.pos[0] = parseFloat(e.target.value); | ||
92 | on_change(new_params); | ||
93 | } | ||
94 | |||
95 | function onPosYChange(e) | ||
96 | { | ||
97 | let new_params = structuredClone(params); | ||
98 | new_params.pos[1] = parseFloat(e.target.value); | ||
99 | on_change(new_params); | ||
100 | } | ||
101 | |||
102 | function onFontSizeChange(e) | ||
103 | { | ||
104 | let new_params = structuredClone(params); | ||
105 | new_params.font_size = parseFloat(e.target.value); | ||
106 | on_change(new_params); | ||
107 | } | ||
108 | |||
109 | function onColorChange(e) | ||
110 | { | ||
111 | let new_params = structuredClone(params); | ||
112 | new_params.color = e.target.value; | ||
113 | on_change(new_params); | ||
114 | } | ||
115 | |||
116 | return h(preact.Fragment, {}, | ||
117 | h("div", {"class": "ButtonRowLeft"}, | ||
118 | h("label", {}, "Content"), | ||
119 | h("input", {type: "text", value: params.content, | ||
120 | onchange: onContentChange}), | ||
121 | h("label", {}, "Color"), | ||
122 | h("input", {type: "color", | ||
123 | value: params.color, | ||
124 | onchange: onColorChange})), | ||
125 | h("div", {"class": "ButtonRowLeft"}, | ||
126 | h("label", {}, "x"), | ||
127 | h("input", {type: "number", value: params.pos[0], | ||
128 | onchange: onPosXChange}), | ||
129 | h("label", {}, "y"), | ||
130 | h("input", {type: "number", value: params.pos[1], | ||
131 | onchange: onPosYChange}), | ||
132 | h("label", {}, "Font size"), | ||
133 | h("input", {type: "number", value: params.font_size, | ||
134 | min: 0, max: 88, step: 0.5, | ||
135 | onchange: onFontSizeChange}))); | ||
136 | } | ||
137 | |||
138 | function EditorViewNetscape({app_state, on_state_change}) | ||
139 | { | ||
140 | const params = app_state.button_params; | ||
141 | function onLogoURLChange(e) | ||
142 | { | ||
143 | let new_state = structuredClone(app_state); | ||
144 | new_state.button_params.logo_url = e.target.value; | ||
145 | on_state_change(new_state); | ||
146 | } | ||
147 | |||
148 | function onBGColorChange(e) | ||
149 | { | ||
150 | let new_state = structuredClone(app_state); | ||
151 | new_state.button_params.color_bg = e.target.value; | ||
152 | on_state_change(new_state); | ||
153 | } | ||
154 | |||
155 | function onTextTopChange(new_params) | ||
156 | { | ||
157 | let new_state = structuredClone(app_state); | ||
158 | new_state.button_params.text_top = new_params; | ||
159 | on_state_change(new_state); | ||
160 | } | ||
161 | |||
162 | function onTextBottomChange(new_params) | ||
163 | { | ||
164 | let new_state = structuredClone(app_state); | ||
165 | new_state.button_params.text_bottom = new_params; | ||
166 | on_state_change(new_state); | ||
167 | } | ||
168 | |||
169 | function onRibbonStyleChange(e) | ||
170 | { | ||
171 | if(app_state.button_params.banner.style == e.target.value) | ||
172 | { | ||
173 | return; | ||
174 | } | ||
175 | let new_state = structuredClone(app_state); | ||
176 | let banner = new_state.button_params.banner; | ||
177 | banner.style = e.target.value; | ||
178 | |||
179 | // With the style change, the text also needs adjustments. We | ||
180 | // will just set it to the style’s default. | ||
181 | if(banner.style == "corner_bottom_right") | ||
182 | { | ||
183 | banner.text.pos = [65, 46]; | ||
184 | } | ||
185 | else if(banner.style == "right") | ||
186 | { | ||
187 | banner.text.pos = [44, 54]; | ||
188 | } | ||
189 | |||
190 | on_state_change(new_state); | ||
191 | } | ||
192 | |||
193 | function onRibbonColorChange(e) | ||
194 | { | ||
195 | let new_state = structuredClone(app_state); | ||
196 | new_state.button_params.banner.color = e.target.value; | ||
197 | on_state_change(new_state); | ||
198 | } | ||
199 | |||
200 | function onRibbonTextChange(new_text) | ||
201 | { | ||
202 | let new_state = structuredClone(app_state); | ||
203 | new_state.button_params.banner.text = new_text; | ||
204 | on_state_change(new_state); | ||
205 | } | ||
206 | |||
207 | return h("div", {}, | ||
208 | h("div", {"class": "ButtonRowLeft"}, | ||
209 | h("label", {}, "Background color"), | ||
210 | h("input", {type: "color", | ||
211 | value: params.color_bg, | ||
212 | onchange: onBGColorChange})), | ||
213 | h("div", {"class": "ButtonRowLeft"}, | ||
214 | h("label", {}, "Logo URL"), | ||
215 | h("input", {type: "url", value: params.logo_url, | ||
216 | onchange: onLogoURLChange})), | ||
217 | h("div", {"class": "LabeledPanel"}, | ||
218 | h("h2", {}, "Main text"), | ||
219 | h(TextControlView, {params: params.text_top, | ||
220 | on_change: onTextTopChange})), | ||
221 | h("div", {"class": "LabeledPanel"}, | ||
222 | h("h2", {}, "Flavor text"), | ||
223 | h(TextControlView, {params: params.text_bottom, | ||
224 | on_change: onTextBottomChange})), | ||
225 | h("div", {"class": "LabeledPanel"}, | ||
226 | h("h2", {}, "Ribbon"), | ||
227 | h("div", {"class": "ButtonRowLeft"}, | ||
228 | h("label", {}, "Style"), | ||
229 | h("select", {value: params.banner.style, | ||
230 | onchange: onRibbonStyleChange}, | ||
231 | h("option", {value: "corner_bottom_right"}, "Bottom right"), | ||
232 | h("option", {value: "right"}, "Vertical right")), | ||
233 | h("label", {}, "Background color"), | ||
234 | h("input", {type: "color", | ||
235 | value: params.banner.color, | ||
236 | onchange: onRibbonColorChange})), | ||
237 | h("div", {"class": "LabeledPanel"}, | ||
238 | h("h2", {}, "Text"), | ||
239 | h(TextControlView, {params: params.banner.text, | ||
240 | on_change: onRibbonTextChange})), | ||
241 | |||
242 | ), | ||
243 | |||
244 | |||
245 | ); | ||
246 | } | ||
247 | |||
248 | function EditorView({app_state}) | ||
249 | { | ||
250 | const [state, setState] = preactHooks.useState(app_state); | ||
251 | let preview_view = null; | ||
252 | let editor_view = null; | ||
253 | if(app_state.button_type == "netscape") | ||
254 | { | ||
255 | preview_view = ButtonViewNetscape; | ||
256 | editor_view = EditorViewNetscape; | ||
257 | } | ||
258 | |||
259 | function onStateChange(new_state) | ||
260 | { | ||
261 | console.debug("New state is", new_state); | ||
262 | setState(new_state); | ||
263 | } | ||
264 | |||
265 | return h("div", {}, | ||
266 | h(preview_view, {app_state: state}), | ||
267 | h("select", {}, | ||
268 | h("option", {value: "netscape"}, "Netscape"), | ||
269 | ), | ||
270 | h(editor_view, {app_state: state, on_state_change: onStateChange})); | ||
271 | } | ||
272 | |||
273 | function App() | ||
274 | { | ||
275 | return h("div", {}, | ||
276 | h(EditorView, {app_state: DEFAULT_APP_STATE})); | ||
277 | } | ||
278 | |||
279 | preact.render(h(App, {}), document.getElementById("App")); | ||
diff --git a/button-maker/scripts/tiles.js b/button-maker/scripts/tiles.js new file mode 100644 index 0000000..f35887a --- /dev/null +++ b/button-maker/scripts/tiles.js | |||
@@ -0,0 +1,94 @@ | |||
1 | const TILES = [ | ||
2 | null, | ||
3 | { | ||
4 | id: "blank", | ||
5 | name: "black", | ||
6 | inner_svg: null, | ||
7 | }, { | ||
8 | id: "teleporter", | ||
9 | name: "teleporter", | ||
10 | inner_svg: h("circle", { | ||
11 | "cx": CELL_SIZE * 0.5, "cy": CELL_SIZE * 0.5, | ||
12 | "r": (CELL_SIZE - 10) * 0.5, "stroke": "black", | ||
13 | "fill": "transparent", "stroke-width": 6, | ||
14 | }), | ||
15 | }, { | ||
16 | id: "fleet_command", | ||
17 | name: "fleet command", | ||
18 | inner_svg: h("polyline", { | ||
19 | "points": `3, ${CELL_SIZE} \ | ||
20 | 3, ${CELL_SIZE - 15} \ | ||
21 | 13, ${CELL_SIZE - 25} \ | ||
22 | 13, ${CELL_SIZE - 5} \ | ||
23 | ${CELL_SIZE - 13}, ${CELL_SIZE - 5} \ | ||
24 | ${CELL_SIZE - 13}, ${CELL_SIZE - 25} \ | ||
25 | ${CELL_SIZE - 3}, ${CELL_SIZE - 15} \ | ||
26 | ${CELL_SIZE - 3}, ${CELL_SIZE}`, | ||
27 | "fill": "black", "stroke-width": 0, | ||
28 | }), | ||
29 | }, { | ||
30 | id: "scanner", | ||
31 | name: "scanner", | ||
32 | inner_svg: | ||
33 | h(preact.Fragment, {}, | ||
34 | h("circle", {"cx": CELL_SIZE * 0.5, "cy": CELL_SIZE * 0.5, | ||
35 | "r": CELL_SIZE * 0.5 - 2, "fill": "transparent", | ||
36 | "stroke": "black", "stroke-width": 2}), | ||
37 | h("circle", {"cx": CELL_SIZE * 0.5, "cy": CELL_SIZE * 0.5, | ||
38 | "r": CELL_SIZE * 0.25, "fill": "transparent", | ||
39 | "stroke": "black", "stroke-width": 2}), | ||
40 | h("line", {"x1": 2, "y1": CELL_SIZE * 0.5, | ||
41 | "x2": CELL_SIZE - 2, "y2": CELL_SIZE * 0.5, | ||
42 | "stroke": "black", "stroke-width": 2}), | ||
43 | h("line", {"y1": 2, "x1": CELL_SIZE * 0.5, | ||
44 | "y2": CELL_SIZE - 2, "x2": CELL_SIZE * 0.5, | ||
45 | "stroke": "black", "stroke-width": 2}), | ||
46 | h("line", {"x1": CELL_SIZE * 0.5, "y1": CELL_SIZE * 0.5, | ||
47 | "x2": CELL_SIZE * 0.5 + (CELL_SIZE * 0.5 - 2) * Math.cos(Math.PI / 6), | ||
48 | "y2": CELL_SIZE * 0.5 - (CELL_SIZE * 0.5 - 2) * Math.sin(Math.PI / 6), | ||
49 | "stroke": "black", "stroke-width": 2}), | ||
50 | h("circle", {"cx": 22, "cy": 6, "r": 2, "fill": "black", | ||
51 | "stroke-width": 0}), | ||
52 | h("circle", {"cx": 10, "cy": 22, "r": 4, "fill": "black", | ||
53 | "stroke-width": 0}), | ||
54 | |||
55 | ), | ||
56 | }, { | ||
57 | id: "extractor", | ||
58 | name: "stellar extractor", | ||
59 | inner_svg: | ||
60 | h(preact.Fragment, {}, | ||
61 | h("circle", {"cx": CELL_SIZE * 0.5, "cy": 21, | ||
62 | "r": 10, "fill": "black", "stroke-width": 0}), | ||
63 | h("polygon", {"points": `${CELL_SIZE * 0.5 - 4}, 20 \ | ||
64 | ${CELL_SIZE * 0.5 - 4}, 10 \ | ||
65 | ${CELL_SIZE * 0.5 - 8}, 10 \ | ||
66 | ${CELL_SIZE * 0.5}, 3 \ | ||
67 | ${CELL_SIZE * 0.5 + 8}, 10 \ | ||
68 | ${CELL_SIZE * 0.5 + 4}, 10 \ | ||
69 | ${CELL_SIZE * 0.5 + 4}, 20`, | ||
70 | "stroke": "black", "stroke-width": 2, | ||
71 | "fill": "white"}), | ||
72 | ), | ||
73 | }, { | ||
74 | id: "storage", | ||
75 | name: "storage", | ||
76 | inner_svg: | ||
77 | h(preact.Fragment, {}, | ||
78 | h("rect", {"x": 4, "y": 4, "width": CELL_SIZE - 8, | ||
79 | "height": CELL_SIZE - 8, "rx": 2, "ry": 2, | ||
80 | "fill": "black", "stroke-width": 0}), | ||
81 | h("line", {"x1": 4, "y1": 14, "x2": CELL_SIZE - 4, "y2": 14, | ||
82 | "stroke": "white", "stroke-width": 2}), | ||
83 | h("rect", {"x": CELL_SIZE * 0.5 - 4, "y": 10, "width": 8, "height": 8, | ||
84 | "fill": "white", "stroke-width": 0}), | ||
85 | ), | ||
86 | }, { | ||
87 | id: "plant", | ||
88 | name: "double cultivation chamber", | ||
89 | inner_svg: h("path", {"d": "m3.02 17.7a13 13 0 0013 13 13 13 0 00-13-13m13 13a13 13 0 0013-13 13 13 0 00-13 13m8.66-27.4v7.21a8.66 8.66 0 01-8.66 8.66 8.66 8.66 0 01-8.66-8.66v-7.21c1.07 0 2.12.173 3.12.563.793.332 1.5.821 2.09 1.44l3.43-3.43 3.43 3.43c.591-.622 1.3-1.11 2.09-1.44.996-.388 2.05-.563 3.12-.563z", | ||
90 | "fill": "black", "stroke-width": 0}), | ||
91 | } | ||
92 | ]; | ||
93 | |||
94 | const TILE_MAP = Object.fromEntries(TILES.slice(1).map(t => [t.id, t])); | ||