diff options
author | MetroWind <chris.corsair@gmail.com> | 2025-09-27 13:05:27 -0700 |
---|---|---|
committer | MetroWind <chris.corsair@gmail.com> | 2025-09-27 13:05:27 -0700 |
commit | 81b539d6a63dd4921686fb50f05daf1b1d725e3b (patch) | |
tree | 5d6d832d73f7f946ebf40924221ffcda980f9837 /button-maker/scripts/main.js | |
parent | 925b5fcea3b85df08eea6574caf76639018a1937 (diff) |
Add button maker. Support basic editing.
Diffstat (limited to 'button-maker/scripts/main.js')
-rw-r--r-- | button-maker/scripts/main.js | 279 |
1 files changed, 279 insertions, 0 deletions
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")); | ||