diff --git a/resources/flags/custom/center_circle.svg b/resources/flags/custom/center_circle.svg new file mode 100644 index 000000000..10d994540 --- /dev/null +++ b/resources/flags/custom/center_circle.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/resources/flags/custom/frame.svg b/resources/flags/custom/frame.svg new file mode 100644 index 000000000..42a52d8b2 --- /dev/null +++ b/resources/flags/custom/frame.svg @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/resources/flags/custom/full.svg b/resources/flags/custom/full.svg new file mode 100644 index 000000000..479b6501c --- /dev/null +++ b/resources/flags/custom/full.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/resources/flags/custom/jp.svg b/resources/flags/custom/jp.svg new file mode 100644 index 000000000..06da6928c --- /dev/null +++ b/resources/flags/custom/jp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/flags/custom/test.svg b/resources/flags/custom/test.svg new file mode 100644 index 000000000..5f10f0c2f --- /dev/null +++ b/resources/flags/custom/test.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/src/client/FlagInput.ts b/src/client/FlagInput.ts index 672f07e63..a1c670578 100644 --- a/src/client/FlagInput.ts +++ b/src/client/FlagInput.ts @@ -3,11 +3,113 @@ import { customElement, state } from "lit/decorators.js"; import Countries from "./data/countries.json"; const flagKey: string = "flag"; +import frame from "../../resources/flags/custom/frame.svg"; + +import center_circle from "../../resources/flags/custom/center_circle.svg"; +import full from "../../resources/flags/custom/full.svg"; +import test from "../../resources/flags/custom/test.svg"; + +const FlagMap: Record = { + frame, + center_circle, + test, + full, +}; + +const LayerShortNames: Record = { + center_circle: "cc", + frame: "fr", + full: "fu", + test: "ts", +}; + +const ColorShortNames: Record = { + "#ff0000": "r", // red + "#ffa500": "o", // orange + "#ffff00": "y", // yellow + "#008000": "g", // green + "#00ffff": "c", // cyan + "#0000ff": "b", // blue + "#800080": "p", // purple + "#ff69b4": "h", // hotpink + "#a52a2a": "br", // brown + "#808080": "gr", // gray + "#000000": "bl", // black + "#ffffff": "w", // white + "#20b2aa": "t", // teal + "#ff6347": "tm", // tomato + "#4682b4": "sb", // steelblue +}; + @customElement("flag-input") export class FlagInput extends LitElement { @state() private flag: string = ""; @state() private search: string = ""; @state() private showModal: boolean = false; + @state() private activeTab: "real" | "custom" = "real"; + @state() private selectedColor: string = "#ff0000"; + @state() private openColorIndex: number | null = null; + + private readonly colorOptions: string[] = [ + "#ff0000", + "#ffa500", + "#ffff00", + "#008000", + "#00ffff", + "#0000ff", + "#800080", + "#ff69b4", + "#a52a2a", + "#808080", + "#000000", + "#ffffff", + "#20b2aa", + "#ff6347", + "#4682b4", + ]; + + @state() private customLayers: { name: string; color: string }[] = []; + + private addLayer(name: string) { + this.customLayers = [ + ...this.customLayers, + { name, color: this.selectedColor }, + ]; + } + + private removeLayer(index: number) { + this.customLayers = this.customLayers.filter((_, i) => i !== index); + } + + private moveLayerUp(index: number) { + if (index === 0) return; + const newLayers = [...this.customLayers]; + [newLayers[index - 1], newLayers[index]] = [ + newLayers[index], + newLayers[index - 1], + ]; + this.customLayers = newLayers; + } + + private moveLayerDown(index: number) { + if (index === this.customLayers.length - 1) return; + const newLayers = [...this.customLayers]; + [newLayers[index], newLayers[index + 1]] = [ + newLayers[index + 1], + newLayers[index], + ]; + this.customLayers = newLayers; + } + + private updateLayerColor(index: number, color: string) { + const newLayers = [...this.customLayers]; + newLayers[index].color = color; + this.customLayers = newLayers; + } + + private toggleColorPicker(index: number) { + this.openColorIndex = this.openColorIndex === index ? null : index; + } static styles = css` @media (max-width: 768px) { @@ -76,65 +178,325 @@ export class FlagInput extends LitElement { render() { return html` -
(this.showModal = false)} - >
+ ${this.showModal ? html`
- -
- ${Countries.filter( - (country) => - country.name - .toLowerCase() - .includes(this.search.toLowerCase()) || - country.code - .toLowerCase() - .includes(this.search.toLowerCase()), - ).map( - (country) => html` - - `, - )} + +
+ +
+ + + ${this.activeTab === "real" + ? html` + +
+ ${Countries.filter( + (country) => + country.name + .toLowerCase() + .includes(this.search.toLowerCase()) || + country.code + .toLowerCase() + .includes(this.search.toLowerCase()), + ).map( + (country) => html` + + `, + )} +
+ ` + : html` +
+ +
+ +
+ ${this.colorOptions.map( + (color) => html` + + `, + )} +
+ + +

+ Select a Layer +

+ + ${Object.entries(FlagMap).map( + ([name, src]) => html` + + `, + )} +
+ + +
+ +

+ Preview +

+
+ ${this.customLayers.map(({ name, color }) => { + const src = FlagMap[name]; + if (!src) return null; + + return html` +
+ `; + })} +
+ + + + +
+

+ Layers +

+
    + ${this.customLayers.map( + ({ name, color }, index) => html` +
  • +
    + + + ${name} + +
    + + + + +
    +
    + + + ${this.openColorIndex === index + ? html` +
    + ${this.colorOptions.map( + (c) => html` + + `, + )} +
    + ` + : ""} +
  • + `, + )} +
+
+
+
+ `}
` : ""}
`; } + + private encodeCustomFlag(): string { + return ( + "ctmfg" + + this.customLayers + .map(({ name, color }) => { + const shortName = LayerShortNames[name] || name; + const shortColor = ColorShortNames[color] || color.replace("#", ""); + return `${shortName}-${shortColor}`; + }) + .join("_") + ); + } + + private isCustomFlag(flag: string): boolean { + return flag.startsWith("ctmfg"); + } + + private decodeCustomFlag(code: string): { name: string; color: string }[] { + if (!this.isCustomFlag(code)) return []; + + const short = code.replace("ctmfg", ""); + const reverseNameMap = Object.fromEntries( + Object.entries(LayerShortNames).map(([k, v]) => [v, k]), + ); + const reverseColorMap = Object.fromEntries( + Object.entries(ColorShortNames).map(([k, v]) => [v, k]), + ); + + return short.split("_").map((segment) => { + const [shortName, shortColor] = segment.split("-"); + const name = reverseNameMap[shortName] || shortName; + const color = reverseColorMap[shortColor] || `#${shortColor}`; + return { name, color }; + }); + } + + private renderFlagPreview(flag: string) { + if (!this.isCustomFlag(flag)) { + return html``; + } + + const layers = this.decodeCustomFlag(flag); + return html` +
+ ${layers.map(({ name, color }) => { + const src = FlagMap[name]; + if (!src) return null; + + return html` +
+ `; + })} +
+ `; + } } diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index 6166c1d76..3a57bf43e 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -180,6 +180,7 @@ export class NameLayer implements Layer { flagImg.classList.add("player-flag"); flagImg.style.opacity = "0.8"; flagImg.src = "/flags/" + player.flag() + ".svg"; + // this.renderPlayerFlag(player.flag(), flagImg); flagImg.style.zIndex = "1"; flagImg.style.aspectRatio = "3/4"; nameDiv.appendChild(flagImg); @@ -216,6 +217,102 @@ export class NameLayer implements Layer { return element; } + renderPlayerFlag(flagCode: string, target: HTMLElement) { + if (!flagCode.startsWith("ctmfg")) { + if (target instanceof HTMLImageElement) { + target.src = "/flags/" + flagCode + ".svg"; + } else { + target.innerHTML = ``; + } + return; + } + + // ctmfg → カスタム旗表示に差し替え + const reverseNameMap = { + cc: "center_circle", + fr: "frame", + fu: "full", + ts: "test", + }; + + const reverseColorMap = { + r: "#ff0000", + o: "#ffa500", + y: "#ffff00", + g: "#008000", + c: "#00ffff", + b: "#0000ff", + p: "#800080", + h: "#ff69b4", + br: "#a52a2a", + gr: "#808080", + bl: "#000000", + w: "#ffffff", + t: "#20b2aa", + tm: "#ff6347", + sb: "#4682b4", + }; + + const flagMap: Record = { + center_circle: "/flags/custom/center_circle.svg", + frame: "/flags/custom/frame.svg", + full: "/flags/custom/full.svg", + test: "/flags/custom/test.svg", + }; + + const code = flagCode.replace("ctmfg", ""); + const layers = code.split("_").map((segment) => { + const [shortName, shortColor] = segment.split("-"); + const name = reverseNameMap[shortName] || shortName; + const color = reverseColorMap[shortColor] || "#" + shortColor; + return { name, color }; + }); + + // img要素だった場合、置き換える + if (target instanceof HTMLImageElement) { + const wrapper = document.createElement("div"); + wrapper.style.width = target.width + "px"; + wrapper.style.height = target.height + "px"; + wrapper.className = target.className; + wrapper.style.position = target.style.position || "relative"; + wrapper.style.zIndex = target.style.zIndex; + wrapper.style.opacity = target.style.opacity; + + // 古い画像を削除 + target.replaceWith(wrapper); + target = wrapper; + } + + // divに描画 + target.innerHTML = ""; + target.style.backgroundColor = "white"; + target.style.overflow = "hidden"; + target.style.position = "relative"; + target.style.aspectRatio = "3/4"; + target.style.border = "1px solid gray"; + + for (const { name, color } of layers) { + const mask = flagMap[name]; + if (!mask) continue; + + const layer = document.createElement("div"); + layer.style.position = "absolute"; + layer.style.top = "0"; + layer.style.left = "0"; + layer.style.width = "100%"; + layer.style.height = "100%"; + layer.style.backgroundColor = color; + layer.style.maskImage = `url(${mask})`; + layer.style.maskRepeat = "no-repeat"; + layer.style.maskPosition = "center"; + layer.style.maskSize = "contain"; + layer.style.webkitMaskImage = `url(${mask})`; + layer.style.webkitMaskRepeat = "no-repeat"; + layer.style.webkitMaskPosition = "center"; + layer.style.webkitMaskSize = "contain"; + target.appendChild(layer); + } + } renderPlayerInfo(render: RenderInfo) { if (!render.player.nameLocation() || !render.player.isAlive()) { this.renders = this.renders.filter((r) => r != render);