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/index.html | 28 ++++ button-maker/scripts/lib.js | 79 ++++++++++ button-maker/scripts/main.js | 279 ++++++++++++++++++++++++++++++++++++ button-maker/scripts/tiles.js | 94 ++++++++++++ button-maker/styles.css | 326 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 806 insertions(+) create mode 100644 button-maker/index.html create mode 100644 button-maker/scripts/lib.js create mode 100644 button-maker/scripts/main.js create mode 100644 button-maker/scripts/tiles.js create mode 100644 button-maker/styles.css (limited to 'button-maker') diff --git a/button-maker/index.html b/button-maker/index.html new file mode 100644 index 0000000..6e5aba0 --- /dev/null +++ b/button-maker/index.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + Button maker + + +
+

Button Maker

+
+
+
+ + 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 @@ +// In Firefox (on Mac only?), for , setting value +// to abbreviated hex form (e.g. #123) doesn’t work. So all colors +// should be specified in full form. +const DEFAULT_NETSCAPE_PARAMS = { + color_bg: "#c0c0c0", + logo_url: "https://upload.wikimedia.org/wikipedia/commons/0/08/Netscape_icon.svg", + text_top: { + content: "Netscape", + pos: [31, 11], + font_size: 10.5, + color: "#000000", + }, + text_bottom: { + content: "Now!", + pos: [30, 28], + font_size: 17.5, + color: "#ff0000", + }, + banner: { + style: "corner_bottom_right", // corner_bottom_right or right + color: "#008080", + text: { + content: "5.0", + pos: [65, 46], + font_size: 8, + color: "#ffffff", + }, + }, +}; + +const DEFAULT_APP_STATE = { + button_type: "netscape", + button_params: DEFAULT_NETSCAPE_PARAMS, +} + +const SVG_ATTRIBUTES = {viewBox: "0 0 88 31", width: 88, height: 31}; + +// Return three integers as an array. +function parseHexColor(color_str) +{ + console.debug("Parsing", color_str); + m = color_str.match(/^#([0-9a-f]{3})$/i); + if(m) + { + // in three-character format, each value is multiplied by 0x11 to give an + // even scale from 0x00 to 0xff + return [ + parseInt(m[1].charAt(0),16)*0x11, + parseInt(m[1].charAt(1),16)*0x11, + parseInt(m[1].charAt(2),16)*0x11 + ]; + } + + m = color_str.match(/^#([0-9a-f]{6})$/i); + if(m) + { + return [ + parseInt(m[1].substr(0,2),16), + parseInt(m[1].substr(2,2),16), + parseInt(m[1].substr(4,2),16) + ]; + } + return null; +} + +function lighten(rgb, delta) +{ + return [rgb[0] + delta, rgb[1] + delta, rgb[2] + delta]; +} + +function darken(rgb, delta) +{ + return [rgb[0] - delta, rgb[1] - delta, rgb[2] - delta]; +} + +function color2Str(color) +{ + return `rgb(${color[0]} ${color[1]} ${color[2]})`; +} 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")); 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 @@ +const TILES = [ + null, + { + id: "blank", + name: "black", + inner_svg: null, + }, { + id: "teleporter", + name: "teleporter", + inner_svg: h("circle", { + "cx": CELL_SIZE * 0.5, "cy": CELL_SIZE * 0.5, + "r": (CELL_SIZE - 10) * 0.5, "stroke": "black", + "fill": "transparent", "stroke-width": 6, + }), + }, { + id: "fleet_command", + name: "fleet command", + inner_svg: h("polyline", { + "points": `3, ${CELL_SIZE} \ +3, ${CELL_SIZE - 15} \ +13, ${CELL_SIZE - 25} \ +13, ${CELL_SIZE - 5} \ +${CELL_SIZE - 13}, ${CELL_SIZE - 5} \ +${CELL_SIZE - 13}, ${CELL_SIZE - 25} \ +${CELL_SIZE - 3}, ${CELL_SIZE - 15} \ +${CELL_SIZE - 3}, ${CELL_SIZE}`, + "fill": "black", "stroke-width": 0, + }), + }, { + id: "scanner", + name: "scanner", + inner_svg: + h(preact.Fragment, {}, + h("circle", {"cx": CELL_SIZE * 0.5, "cy": CELL_SIZE * 0.5, + "r": CELL_SIZE * 0.5 - 2, "fill": "transparent", + "stroke": "black", "stroke-width": 2}), + h("circle", {"cx": CELL_SIZE * 0.5, "cy": CELL_SIZE * 0.5, + "r": CELL_SIZE * 0.25, "fill": "transparent", + "stroke": "black", "stroke-width": 2}), + h("line", {"x1": 2, "y1": CELL_SIZE * 0.5, + "x2": CELL_SIZE - 2, "y2": CELL_SIZE * 0.5, + "stroke": "black", "stroke-width": 2}), + h("line", {"y1": 2, "x1": CELL_SIZE * 0.5, + "y2": CELL_SIZE - 2, "x2": CELL_SIZE * 0.5, + "stroke": "black", "stroke-width": 2}), + h("line", {"x1": CELL_SIZE * 0.5, "y1": CELL_SIZE * 0.5, + "x2": CELL_SIZE * 0.5 + (CELL_SIZE * 0.5 - 2) * Math.cos(Math.PI / 6), + "y2": CELL_SIZE * 0.5 - (CELL_SIZE * 0.5 - 2) * Math.sin(Math.PI / 6), + "stroke": "black", "stroke-width": 2}), + h("circle", {"cx": 22, "cy": 6, "r": 2, "fill": "black", + "stroke-width": 0}), + h("circle", {"cx": 10, "cy": 22, "r": 4, "fill": "black", + "stroke-width": 0}), + + ), + }, { + id: "extractor", + name: "stellar extractor", + inner_svg: + h(preact.Fragment, {}, + h("circle", {"cx": CELL_SIZE * 0.5, "cy": 21, + "r": 10, "fill": "black", "stroke-width": 0}), + h("polygon", {"points": `${CELL_SIZE * 0.5 - 4}, 20 \ +${CELL_SIZE * 0.5 - 4}, 10 \ +${CELL_SIZE * 0.5 - 8}, 10 \ +${CELL_SIZE * 0.5}, 3 \ +${CELL_SIZE * 0.5 + 8}, 10 \ +${CELL_SIZE * 0.5 + 4}, 10 \ +${CELL_SIZE * 0.5 + 4}, 20`, + "stroke": "black", "stroke-width": 2, + "fill": "white"}), + ), + }, { + id: "storage", + name: "storage", + inner_svg: + h(preact.Fragment, {}, + h("rect", {"x": 4, "y": 4, "width": CELL_SIZE - 8, + "height": CELL_SIZE - 8, "rx": 2, "ry": 2, + "fill": "black", "stroke-width": 0}), + h("line", {"x1": 4, "y1": 14, "x2": CELL_SIZE - 4, "y2": 14, + "stroke": "white", "stroke-width": 2}), + h("rect", {"x": CELL_SIZE * 0.5 - 4, "y": 10, "width": 8, "height": 8, + "fill": "white", "stroke-width": 0}), + ), + }, { + id: "plant", + name: "double cultivation chamber", + 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", + "fill": "black", "stroke-width": 0}), + } +]; + +const TILE_MAP = Object.fromEntries(TILES.slice(1).map(t => [t.id, t])); diff --git a/button-maker/styles.css b/button-maker/styles.css new file mode 100644 index 0000000..bee2d4b --- /dev/null +++ b/button-maker/styles.css @@ -0,0 +1,326 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: unset; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +/* ========== Actual styles ======================================> */ + +:root +{ + --color-bg: #c0c0c0; + --color-fg: black; + --color-border-dark-1: #808080; + --color-border-dark-2: black; + --color-border-light-1: #dfdfdf; + --color-border-light-2: white; + --color-titlebar-1: navy; + --color-titlebar-2: #1084d0; +} + +* +{ + box-sizing: border-box; +} + +html +{ + background: #008080; + color: var(--color-fg); + font-family: sans-serif; + font-size: 12px; + padding: 12px; +} + +.Window +{ + box-shadow: inset -1px -1px var(--color-border-dark-2), + inset 1px 1px var(--color-border-light-1), + inset -2px -2px var(--color-border-dark-1), + inset 2px 2px var(--color-border-light-2); + background: var(--color-bg); + padding: 10px; + + .Titlebar + { + background: linear-gradient(90deg, var(--color-titlebar-1), var(--color-titlebar-2)); + margin: -6px -6px 10px -6px; + color: white; + padding: 3px; + } +} + +.Dialog +{ + max-width: 910px; + margin: 0px auto; +} + +.LabeledPanel +{ + border-top: solid var(--color-border-light-1) 1px; + border-right: solid var(--color-border-dark-1) 1px; + border-bottom: solid var(--color-border-dark-1) 1px; + border-left: solid var(--color-border-light-1) 1px; + + box-shadow: 1px 1px var(--color-border-light-1), + -1px -1px var(--color-border-dark-1); + padding: 1ex; + padding-top: 0px; + margin: 2ex 0px; +} + +.LabeledPanel > h3, .LabeledPanel > h2 +{ + font-weight: normal; + display: inline-block; + position: relative; + top: calc(-0.5em); + left: 1ex; + background-color: var(--color-bg); + padding: 0px 1ex 0px 1ex; + margin: 0px; +} + +input[type="number"] +{ + appearance: none; + border: none; + outline: none; + box-shadow: inset -1px -1px var(--color-border-light-2), + inset 1px 1px var(--color-border-dark-1), + inset -2px -2px var(--color-border-light-1), + inset 2px 2px var(--color-border-dark-2); + padding: 4px 0px 4px 6px; + font-family: monospace; + font-size: 0.9rem; +} + +input[type="text"], input[type="url"], input[type="email"], textarea +{ + appearance: none; + border: none; + outline: none; + box-shadow: inset -1px -1px var(--color-border-light-2), + inset 1px 1px var(--color-border-dark-1), + inset -2px -2px var(--color-border-light-1), + inset 2px 2px var(--color-border-dark-2); + width: 100%; + padding: 4px 6px; + font-family: monospace; + font-size: 0.9rem; +} + +a.Button, a.FloatButton +{ + color: black; + text-decoration: none; +} + +button, input[type="submit"], input[type="color"] +{ + appearance: none; + border: none; + border-radius: 0px; + outline: none; + box-shadow: inset -1px -1px var(--color-border-dark-2), + inset 1px 1px var(--color-border-light-1), + inset -2px -2px var(--color-border-dark-1), + inset 2px 2px var(--color-border-light-2); + background: var(--color-bg); + text-align: center; +} + +button, input[type="submit"] +{ + min-height: 23px; + min-width: 75px; + padding: 0 12px; +} + +input[type="color"] +{ + padding: 4px; + width: 3em; + height: 2em; +} + +.IconButton +{ + padding: 4px; + min-width: unset; + min-height: unset; +} + +.FloatButton +{ + padding: 4px; + min-width: unset; + min-height: unset; + box-shadow: none; +} + +.FloatButton:hover +{ + padding: 4px; + min-width: unset; + min-height: unset; + box-shadow: inset 1px 1px var(--color-border-light-1), + inset -1px -1px var(--color-border-dark-1); +} + +button:active, .FloatButton:active +{ + box-shadow: inset -1px -1px var(--color-border-light-2), + inset 1px 1px var(--color-border-dark-1), + inset -2px -2px var(--color-border-light-1), + inset 2px 2px var(--color-border-dark-2); +} + +hr +{ + border: none; + border-top: solid var(--color-border-dark-1) 1px; + border-bottom: solid var(--color-border-light-1) 1px; +} + +.ButtonRow, .ButtonRowLeft +{ + display: flex; + gap: 10px; + margin: 10px 0; + align-items: baseline; +} + +.ButtonRow +{ + justify-content: right; +} + +.Toolbar +{ + display: flex; + border-bottom: solid var(--color-border-dark-1) 1px; + box-shadow: 0px 1px var(--color-border-light-1); + position: relative; + left: -10px; + top: -10px; + width: calc(100% + 2ex); + padding: 2px 8px; +} + +table.InputFields +{ + border-collapse: collapse; + width: 100%; + table-layout: auto; + + td + { + padding: 2px 2px; + } + + td:nth-child(2) + { + width: 100%; + } + +} + +.StatusBar +{ + margin: 10px -6px -7px -6px; + display: flex; +} + +.StatusCell +{ + box-shadow: inset -2px -2px var(--color-border-light-2), + inset 2px 2px var(--color-border-dark-1); + padding: 4px; + flex-grow: 1; +} + +#Links +{ + box-shadow: inset -1px -1px var(--color-border-light-2), + inset 1px 1px var(--color-border-dark-1), + inset -2px -2px var(--color-border-light-1), + inset 2px 2px var(--color-border-dark-2); + padding: 2px; +} + +table.TableView +{ + background-color: white; + border-collapse: collapse; + width: 100%; + + thead th + { + box-shadow: inset -1px -1px var(--color-border-dark-2), + inset 1px 1px var(--color-border-light-1), + inset -2px -2px var(--color-border-dark-1), + inset 2px 2px var(--color-border-light-2); + background-color: var(--color-bg); + padding: 2px 4px 4px 4px; + text-align: left; + } + + td + { + padding: 2px; + } +} + +/* ========== App-specific =======================================> */ + +#MainWindow +{ + max-width: 600px; + margin-left: auto; + margin-right: auto; +} -- cgit v1.2.3-70-g09d2