aboutsummaryrefslogtreecommitdiff
path: root/button-maker/scripts/main.js
diff options
context:
space:
mode:
authorMetroWind <chris.corsair@gmail.com>2025-09-27 13:05:27 -0700
committerMetroWind <chris.corsair@gmail.com>2025-09-27 13:05:27 -0700
commit81b539d6a63dd4921686fb50f05daf1b1d725e3b (patch)
tree5d6d832d73f7f946ebf40924221ffcda980f9837 /button-maker/scripts/main.js
parent925b5fcea3b85df08eea6574caf76639018a1937 (diff)
Add button maker. Support basic editing.
Diffstat (limited to 'button-maker/scripts/main.js')
-rw-r--r--button-maker/scripts/main.js279
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 @@
1const h = preact.h;
2
3function 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
78function 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
138function 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
248function 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
273function App()
274{
275 return h("div", {},
276 h(EditorView, {app_state: DEFAULT_APP_STATE}));
277}
278
279preact.render(h(App, {}), document.getElementById("App"));