This commit is contained in:
Aotumuri
2025-04-07 07:35:29 +09:00
parent 5f364adc33
commit c10a83c5ca
7 changed files with 556 additions and 44 deletions
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_レイヤー_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 72 72">
<!-- Generator: Adobe Illustrator 29.4.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 152) -->
<defs>
<style>
.st0 {
fill: #e60012;
}
</style>
</defs>
<circle class="st0" cx="36" cy="36" r="9"/>
</svg>

After

Width:  |  Height:  |  Size: 375 B

+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_レイヤー_1" data-name="レイヤー_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 72 72">
<!-- Generator: Adobe Illustrator 29.4.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 152) -->
<defs>
<style>
.st0 {
fill: none;
stroke: #000;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2px;
}
</style>
</defs>
<path class="st0" d="M5,17h62v38H5V17Z"/>
</svg>

After

Width:  |  Height:  |  Size: 509 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_レイヤー_1" data-name="レイヤー_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 72 72">
<!-- Generator: Adobe Illustrator 29.4.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 152) -->
<defs>
<style>
.st0 {
fill: #e60012;
}
</style>
</defs>
<path class="st0" d="M5,17h62v38H5V17Z"/>
</svg>

After

Width:  |  Height:  |  Size: 400 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><path fill="#fff" d="M5 17h62v38H5z"/><circle cx="36" cy="36" r="9" fill="#d22f27"/><path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 17h62v38H5z"/></svg>

After

Width:  |  Height:  |  Size: 266 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_レイヤー_1" data-name="レイヤー_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 72 72">
<!-- Generator: Adobe Illustrator 29.4.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 152) -->
<defs>
<style>
.st0 {
fill: #e60012;
}
</style>
</defs>
<path class="st0" d="M5,55V17h62"/>
</svg>

After

Width:  |  Height:  |  Size: 394 B

+406 -44
View File
@@ -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<string, string> = {
frame,
center_circle,
test,
full,
};
const LayerShortNames: Record<string, string> = {
center_circle: "cc",
frame: "fr",
full: "fu",
test: "ts",
};
const ColorShortNames: Record<string, string> = {
"#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`
<div
class="absolute left-0 top-0 w-full h-full ${this.showModal
? ""
: "hidden"}"
@click=${() => (this.showModal = false)}
></div>
<div class="flex relative">
<button
@click=${() => (this.showModal = !this.showModal)}
@click=${() => (this.showModal = true)}
class="border p-[4px] rounded-lg flex cursor-pointer border-black/30 dark:border-gray-300/60 bg-white/70 dark:bg-[rgba(55,65,81,0.7)]"
title="Pick a flag!"
>
<img class="size-[48px]" src="/flags/${this.flag || "xx"}.svg" />
${this.renderFlagPreview(this.flag)}
</button>
${this.showModal
? html`
<div
class="text-white flex flex-col gap-[0.5rem] absolute top-[60px] left-[0px] w-[780%] h-[500px] max-h-[50vh] max-w-[87vw] bg-gray-900/80 backdrop-blur-md p-[10px] rounded-[8px] z-[3] ${this
.showModal
? ""
: "hidden"}"
class="text-white flex flex-col gap-[0.5rem] absolute top-[60px] left-[0px] w-[780%] h-[500px] max-h-[50vh] max-w-[87vw] bg-gray-900/80 backdrop-blur-md p-[10px] rounded-[8px] z-[3]"
>
<input
class="h-[2rem] border-none text-center border border-gray-300 rounded-xl shadow-sm text-2xl text-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-black dark:border-gray-300/60 dark:bg-gray-700 dark:text-white"
type="text"
placeholder="Search..."
@change=${this.handleSearch}
@keyup=${this.handleSearch}
/>
<div
class="flex flex-wrap justify-evenly gap-[1rem] overflow-y-auto overflow-x-hidden"
>
${Countries.filter(
(country) =>
country.name
.toLowerCase()
.includes(this.search.toLowerCase()) ||
country.code
.toLowerCase()
.includes(this.search.toLowerCase()),
).map(
(country) => html`
<button
@click=${() => this.setFlag(country.code)}
class="text-center cursor-pointer border-none bg-none opacity-70 sm:w-[calc(33.3333%-15px) w-[calc(100%/3-15px)] md:w-[calc(100%/4-15px)]"
>
<img
class="country-flag w-full h-auto"
src="/flags/${country.code}.svg"
/>
<span class="country-name">${country.name}</span>
</button>
`,
)}
<!-- タブボタン -->
<div class="flex gap-2 mb-2">
<button
class="px-4 py-1 rounded-lg font-bold ${this.activeTab ===
"real"
? "bg-blue-500 text-white"
: "bg-gray-300 text-black"}"
@click=${() => (this.activeTab = "real")}
>
Real Flags
</button>
<button
class="px-4 py-1 rounded-lg font-bold ${this.activeTab ===
"custom"
? "bg-blue-500 text-white"
: "bg-gray-300 text-black"}"
@click=${() => (this.activeTab = "custom")}
>
Custom Flags
</button>
</div>
<!-- 実在する旗の表示 -->
${this.activeTab === "real"
? html`
<input
class="h-[2rem] border-none text-center border border-gray-300 rounded-xl shadow-sm text-2xl text-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-black dark:border-gray-300/60 dark:bg-gray-700 dark:text-white"
type="text"
placeholder="Search..."
@change=${this.handleSearch}
@keyup=${this.handleSearch}
/>
<div
class="flex flex-wrap justify-evenly gap-[1rem] overflow-y-auto overflow-x-hidden"
>
${Countries.filter(
(country) =>
country.name
.toLowerCase()
.includes(this.search.toLowerCase()) ||
country.code
.toLowerCase()
.includes(this.search.toLowerCase()),
).map(
(country) => html`
<button
@click=${() => this.setFlag(country.code)}
class="text-center cursor-pointer border-none bg-none opacity-70 sm:w-[calc(33.3333%-15px)] w-[calc(100%/3-15px)] md:w-[calc(100%/4-15px)]"
>
<img
class="country-flag w-full h-auto"
src="/flags/${country.code}.svg"
/>
<span class="country-name">${country.name}</span>
</button>
`,
)}
</div>
`
: html`
<div class="grid grid-cols-3 gap-4 p-2 h-full">
<!-- 左:カラーパレットと素材選択 -->
<div
class="flex flex-col items-center gap-2 overflow-y-auto"
>
<!-- カラーパレット -->
<div class="flex flex-wrap gap-1 mb-2 justify-center">
${this.colorOptions.map(
(color) => html`
<button
class="w-6 h-6 rounded-full border-2 ${this
.selectedColor === color
? "border-white"
: "border-gray-400"}"
style="background-color: ${color};"
@click=${() => (this.selectedColor = color)}
title=${color}
></button>
`,
)}
</div>
<!-- 素材選択 -->
<p class="text-lg font-bold text-white">
Select a Layer
</p>
${Object.entries(FlagMap).map(
([name, src]) => html`
<button @click=${() => this.addLayer(name)}>
<div
class="w-16 h-10 rounded"
style="
background-color: ${this.selectedColor};
-webkit-mask: url(${src}) center / contain no-repeat;
mask: url(${src}) center / contain no-repeat;
"
title=${name}
></div>
</button>
`,
)}
</div>
<!-- 中央:プレビューとレイヤー一覧 -->
<div
class="col-span-2 flex flex-col items-center h-full overflow-hidden"
>
<!-- プレビュー -->
<p class="text-lg font-bold text-white mb-2">
Preview
</p>
<div
class="relative w-[160px] h-[100px] border border-gray-400 rounded bg-white"
>
${this.customLayers.map(({ name, color }) => {
const src = FlagMap[name];
if (!src) return null;
return html`
<div
class="absolute top-0 left-0 w-full h-full"
style="
background-color: ${color};
-webkit-mask: url(${src}) center / contain no-repeat;
mask: url(${src}) center / contain no-repeat;
"
></div>
`;
})}
</div>
<!-- コードボタン -->
<button
@click=${() => {
const code = this.encodeCustomFlag();
navigator.clipboard.writeText(code);
alert("Copied: " + code);
this.setFlag(code);
}}
class="mt-2 px-4 py-1 bg-green-600 text-white rounded hover:bg-green-500"
>
Copy Flag Code
</button>
<!-- レイヤー一覧 -->
<div
class="mt-4 w-full max-h-[150px] overflow-y-auto"
>
<p class="text-lg font-bold text-white mb-2">
Layers
</p>
<ul class="text-white space-y-1">
${this.customLayers.map(
({ name, color }, index) => html`
<li
class="flex flex-col gap-1 py-1 px-2 bg-gray-800 rounded"
>
<div
class="flex justify-between items-center"
>
<span class="flex items-center gap-2">
<span
class="inline-block w-4 h-4 rounded-full"
style="background-color: ${color};"
></span>
${name}
</span>
<div class="flex gap-1">
<button
@click=${() =>
this.moveLayerUp(index)}
title="Move Up"
class="text-sm px-2 py-1 bg-gray-600 rounded hover:bg-gray-500"
>
</button>
<button
@click=${() =>
this.moveLayerDown(index)}
title="Move Down"
class="text-sm px-2 py-1 bg-gray-600 rounded hover:bg-gray-500"
>
</button>
<button
@click=${() =>
this.toggleColorPicker(index)}
title="Change Color"
class="text-sm px-2 py-1 bg-blue-600 rounded hover:bg-blue-500 text-white"
>
Color
</button>
<button
@click=${() =>
this.removeLayer(index)}
class="text-red-400 hover:text-red-600"
>
Remove
</button>
</div>
</div>
<!-- カラーパレット(表示中のものだけ) -->
${this.openColorIndex === index
? html`
<div
class="flex flex-wrap gap-1 justify-start mt-1"
>
${this.colorOptions.map(
(c) => html`
<button
class="w-3 h-3 rounded-full border ${color ===
c
? "border-white"
: "border-gray-500"}"
style="background-color: ${c};"
@click=${() =>
this.updateLayerColor(
index,
c,
)}
title=${c}
></button>
`,
)}
</div>
`
: ""}
</li>
`,
)}
</ul>
</div>
</div>
</div>
`}
</div>
`
: ""}
</div>
`;
}
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`<img class="size-[48px]" src="/flags/${flag || "xx"}.svg" />`;
}
const layers = this.decodeCustomFlag(flag);
return html`
<div
class="size-[48px] relative border border-gray-300 rounded overflow-hidden bg-white"
>
${layers.map(({ name, color }) => {
const src = FlagMap[name];
if (!src) return null;
return html`
<div
class="absolute top-0 left-0 w-full h-full"
style="
background-color: ${color};
-webkit-mask: url(${src}) center / contain no-repeat;
mask: url(${src}) center / contain no-repeat;
"
></div>
`;
})}
</div>
`;
}
}
+97
View File
@@ -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 = `<img src="/flags/${flagCode}.svg" class="w-full h-full" />`;
}
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<string, string> = {
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);