From 81b539d6a63dd4921686fb50f05daf1b1d725e3b Mon Sep 17 00:00:00 2001 From: MetroWind Date: Sat, 27 Sep 2025 13:05:27 -0700 Subject: Add button maker. Support basic editing. --- button-maker/scripts/main.js | 279 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 button-maker/scripts/main.js (limited to 'button-maker/scripts/main.js') 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 @@ +const h = preact.h; + +function ButtonViewNetscape({app_state}) +{ + const params = app_state.button_params; + console.debug("Rendering netscape params", params); + let color_bg = parseHexColor(params.color_bg); + let color_hilight_1 = color2Str(lighten(color_bg, 63)); + let color_hilight_2 = color2Str(lighten(color_bg, 31)); + let color_shadow_1 = color2Str(darken(color_bg, 0xc0 - 0xa)); + let color_shadow_2 = color2Str(darken(color_bg, 0xc0 - 0x54)); + + let ribbon = null; + let ribbon_text = null; + if(app_state.button_params.banner.style == "corner_bottom_right") + { + ribbon = h("polygon", {points: "86,8 86,24 81,29 65,29", + fill: params.banner.color}); + ribbon_text = h("text", { + x: params.banner.text.pos[0], y: params.banner.text.pos[1], + "font-family": "sans-serif", + "dominant-baseline": "middle", "text-anchor": "middle", + "transform-origin": "center", "transform":"rotate(-45)", + "font-weight": "bold", "font-size": params.banner.text.font_size, + fill: params.banner.text.color, "stroke-width": 0}, + params.banner.text.content); + } + else if(app_state.button_params.banner.style == "right") + { + ribbon = h("rect", {x: 78, y: 2, width: 8, height: 27, + fill: app_state.button_params.banner.color}); + ribbon_text = h("text", { + x: params.banner.text.pos[0], y: params.banner.text.pos[1], + "font-family": "sans-serif", "dominant-baseline": "middle", + "text-anchor": "middle", "transform-origin": "center", + transform: "rotate(-90)", "font-weight": "bold", + "font-size": params.banner.text.font_size, + fill: params.banner.text.color, "stroke-width": 0}, + params.banner.text.content); + } + + return h("svg", SVG_ATTRIBUTES, + h("g", {"stroke-width": 0}, + h("polygon", {points: "0,0 88,0 87,1 1,1", + fill: color_hilight_1}), + h("polygon", {points: "1,1 87,1 86,2 2,2", + fill: color_hilight_2}), + h("polygon", {points: "88,0 88,31 87,30 87,1", + fill: color_shadow_1}), + h("polygon", {points: "87,1 87,30 86,29 86,2", + fill: color_shadow_2}), + h("polygon", {points: "88,31 0,31 1,30 87,30", + fill: color_shadow_1}), + h("polygon", {points: "87,30 1,30 2,29,86,29", + fill: color_shadow_2}), + h("polygon", {points: "0,0 1,1 1,30 0,31", + fill: color_hilight_1}), + h("polygon", {points: "1,1 2,2 2,29 1,30", + fill: color_hilight_2}), + h("rect", {x: 2, y: 2, width: 84, height: 27, + fill: params.color_bg}), + ribbon), + h("image", {href: params.logo_url, x: 3, y: 3, width: 25, + height: 25}), + h("text", {x: params.text_bottom.pos[0], + y: params.text_bottom.pos[1], "font-family": "Kalam", + "font-size": params.text_bottom.font_size, + fill: params.text_bottom.color, "stroke-width": 0}, + "Now!"), + h("text", {x: params.text_top.pos[0], y: params.text_top.pos[1], + "font-family": "sans-serif", + "font-size": params.text_top.font_size, + fill: params.text_top.color, "stroke-width": 0}, + params.text_top.content), + ribbon_text); +} + +function TextControlView({params, on_change}) +{ + console.debug("Rendering text params", params); + function onContentChange(e) + { + let new_params = structuredClone(params); + new_params.content = e.target.value; + on_change(new_params); + } + + function onPosXChange(e) + { + let new_params = structuredClone(params); + new_params.pos[0] = parseFloat(e.target.value); + on_change(new_params); + } + + function onPosYChange(e) + { + let new_params = structuredClone(params); + new_params.pos[1] = parseFloat(e.target.value); + on_change(new_params); + } + + function onFontSizeChange(e) + { + let new_params = structuredClone(params); + new_params.font_size = parseFloat(e.target.value); + on_change(new_params); + } + + function onColorChange(e) + { + let new_params = structuredClone(params); + new_params.color = e.target.value; + on_change(new_params); + } + + return h(preact.Fragment, {}, + h("div", {"class": "ButtonRowLeft"}, + h("label", {}, "Content"), + h("input", {type: "text", value: params.content, + onchange: onContentChange}), + h("label", {}, "Color"), + h("input", {type: "color", + value: params.color, + onchange: onColorChange})), + h("div", {"class": "ButtonRowLeft"}, + h("label", {}, "x"), + h("input", {type: "number", value: params.pos[0], + onchange: onPosXChange}), + h("label", {}, "y"), + h("input", {type: "number", value: params.pos[1], + onchange: onPosYChange}), + h("label", {}, "Font size"), + h("input", {type: "number", value: params.font_size, + min: 0, max: 88, step: 0.5, + onchange: onFontSizeChange}))); +} + +function EditorViewNetscape({app_state, on_state_change}) +{ + const params = app_state.button_params; + function onLogoURLChange(e) + { + let new_state = structuredClone(app_state); + new_state.button_params.logo_url = e.target.value; + on_state_change(new_state); + } + + function onBGColorChange(e) + { + let new_state = structuredClone(app_state); + new_state.button_params.color_bg = e.target.value; + on_state_change(new_state); + } + + function onTextTopChange(new_params) + { + let new_state = structuredClone(app_state); + new_state.button_params.text_top = new_params; + on_state_change(new_state); + } + + function onTextBottomChange(new_params) + { + let new_state = structuredClone(app_state); + new_state.button_params.text_bottom = new_params; + on_state_change(new_state); + } + + function onRibbonStyleChange(e) + { + if(app_state.button_params.banner.style == e.target.value) + { + return; + } + let new_state = structuredClone(app_state); + let banner = new_state.button_params.banner; + banner.style = e.target.value; + + // With the style change, the text also needs adjustments. We + // will just set it to the style’s default. + if(banner.style == "corner_bottom_right") + { + banner.text.pos = [65, 46]; + } + else if(banner.style == "right") + { + banner.text.pos = [44, 54]; + } + + on_state_change(new_state); + } + + function onRibbonColorChange(e) + { + let new_state = structuredClone(app_state); + new_state.button_params.banner.color = e.target.value; + on_state_change(new_state); + } + + function onRibbonTextChange(new_text) + { + let new_state = structuredClone(app_state); + new_state.button_params.banner.text = new_text; + on_state_change(new_state); + } + + return h("div", {}, + h("div", {"class": "ButtonRowLeft"}, + h("label", {}, "Background color"), + h("input", {type: "color", + value: params.color_bg, + onchange: onBGColorChange})), + h("div", {"class": "ButtonRowLeft"}, + h("label", {}, "Logo URL"), + h("input", {type: "url", value: params.logo_url, + onchange: onLogoURLChange})), + h("div", {"class": "LabeledPanel"}, + h("h2", {}, "Main text"), + h(TextControlView, {params: params.text_top, + on_change: onTextTopChange})), + h("div", {"class": "LabeledPanel"}, + h("h2", {}, "Flavor text"), + h(TextControlView, {params: params.text_bottom, + on_change: onTextBottomChange})), + h("div", {"class": "LabeledPanel"}, + h("h2", {}, "Ribbon"), + h("div", {"class": "ButtonRowLeft"}, + h("label", {}, "Style"), + h("select", {value: params.banner.style, + onchange: onRibbonStyleChange}, + h("option", {value: "corner_bottom_right"}, "Bottom right"), + h("option", {value: "right"}, "Vertical right")), + h("label", {}, "Background color"), + h("input", {type: "color", + value: params.banner.color, + onchange: onRibbonColorChange})), + h("div", {"class": "LabeledPanel"}, + h("h2", {}, "Text"), + h(TextControlView, {params: params.banner.text, + on_change: onRibbonTextChange})), + + ), + + + ); +} + +function EditorView({app_state}) +{ + const [state, setState] = preactHooks.useState(app_state); + let preview_view = null; + let editor_view = null; + if(app_state.button_type == "netscape") + { + preview_view = ButtonViewNetscape; + editor_view = EditorViewNetscape; + } + + function onStateChange(new_state) + { + console.debug("New state is", new_state); + setState(new_state); + } + + return h("div", {}, + h(preview_view, {app_state: state}), + h("select", {}, + h("option", {value: "netscape"}, "Netscape"), + ), + h(editor_view, {app_state: state, on_state_change: onStateChange})); +} + +function App() +{ + return h("div", {}, + h(EditorView, {app_state: DEFAULT_APP_STATE})); +} + +preact.render(h(App, {}), document.getElementById("App")); -- cgit v1.2.3-70-g09d2