diff --git a/public/Skins/ency.png b/public/Skins/ency.png new file mode 100644 index 0000000..a5f8ee5 Binary files /dev/null and b/public/Skins/ency.png differ diff --git a/public/Skins/individul.png b/public/Skins/individul.png new file mode 100644 index 0000000..f31ba40 Binary files /dev/null and b/public/Skins/individul.png differ diff --git a/src/components/common/SkinModel3D.tsx b/src/components/common/SkinModel3D.tsx new file mode 100644 index 0000000..698a7bb --- /dev/null +++ b/src/components/common/SkinModel3D.tsx @@ -0,0 +1,614 @@ +import { useEffect, useRef, memo } from "react"; +import * as THREE from "three"; +type UVSet = Record; +interface SkinModel3DProps { + src: string; + slim?: boolean; + capeUrl?: string | null; + animate?: boolean; + className?: string; +} + +const clamp = (v: number, min: number, max: number) => + Math.min(max, Math.max(min, v)); + +let snapshotChain: Promise = Promise.resolve(); +const snapshotCache = new Map(); +function createFaceMaterial( + tex: THREE.Texture, + x: number, + y: number, + w: number, + h: number, + flipX = false, + flipY = false, +) { + const img = tex.image as { width: number; height: number } | undefined; + const imgH = img && img.height ? img.height : 64; + const matTex = tex.clone(); + matTex.repeat.set((flipX ? -w : w) / 64, (flipY ? -h : h) / imgH); + matTex.offset.set((flipX ? x + w : x) / 64, 1 - (flipY ? y : y + h) / imgH); + matTex.needsUpdate = true; + return new THREE.MeshLambertMaterial({ + map: matTex, + transparent: true, + alphaTest: 0.5, + side: THREE.FrontSide, + }); +} + +function createPart( + tex: THREE.Texture, + w: number, + h: number, + d: number, + uv: UVSet, + overlayUv?: UVSet, + swapMats = false, + isLegacyMirror = false, +) { + const group = new THREE.Group(); + const geo = new THREE.BoxGeometry(w, h, d); + const getMats = (uvSet: UVSet) => { + const flipX = isLegacyMirror; + return [ + createFaceMaterial( + tex, + swapMats ? uvSet.right[0] : uvSet.left[0], + uvSet.left[1], + uvSet.left[2], + uvSet.left[3], + flipX, + ), + createFaceMaterial( + tex, + swapMats ? uvSet.left[0] : uvSet.right[0], + uvSet.right[1], + uvSet.right[2], + uvSet.right[3], + flipX, + ), + createFaceMaterial( + tex, + uvSet.top[0], + uvSet.top[1], + uvSet.top[2], + uvSet.top[3], + flipX, + true, + ), + createFaceMaterial( + tex, + uvSet.bottom[0], + uvSet.bottom[1], + uvSet.bottom[2], + uvSet.bottom[3], + flipX, + true, + ), + createFaceMaterial( + tex, + uvSet.front[0], + uvSet.front[1], + uvSet.front[2], + uvSet.front[3], + flipX, + ), + createFaceMaterial( + tex, + uvSet.back[0], + uvSet.back[1], + uvSet.back[2], + uvSet.back[3], + !flipX, + ), + ]; + }; + group.add(new THREE.Mesh(geo, getMats(uv))); + if (overlayUv) { + const oGeo = new THREE.BoxGeometry(w + 0.5, h + 0.5, d + 0.5); + group.add(new THREE.Mesh(oGeo, getMats(overlayUv))); + } + return group; +} + +const limbUv = (x: number, y: number, w = 4): UVSet => ({ + top: [x + 4, y, w, 4], + bottom: [x + 4 + w, y, w, 4], + right: [x, y + 4, 4, 12], + front: [x + 4, y + 4, w, 12], + left: [x + 4 + w, y + 4, 4, 12], + back: [x + 8 + w, y + 4, w, 12], +}); + +function buildPlayer( + playerGroup: THREE.Group, + texture: THREE.Texture, + slimOpt?: boolean, +) { + const img = texture.image as HTMLImageElement; + const isLegacy = (img?.height || 64) === 32; + const isSlim = + slimOpt !== undefined + ? slimOpt + : !isLegacy && + (() => { + const canvas = document.createElement("canvas"); + canvas.width = img?.width || 64; + canvas.height = img?.height || 64; + const ctx = canvas.getContext("2d"); + if (!ctx) return false; + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + try { + return ctx.getImageData(42, 48, 1, 1).data[3] === 0; + } catch { + return false; + } + })(); + const armW = isSlim ? 3 : 4; + const headUv: UVSet = { + top: [8, 0, 8, 8], + bottom: [16, 0, 8, 8], + right: [0, 8, 8, 8], + left: [16, 8, 8, 8], + front: [8, 8, 8, 8], + back: [24, 8, 8, 8], + }; + const hatUv: UVSet = { + top: [40, 0, 8, 8], + bottom: [48, 0, 8, 8], + right: [32, 8, 8, 8], + left: [48, 8, 8, 8], + front: [40, 8, 8, 8], + back: [56, 8, 8, 8], + }; + const head = createPart(texture, 8, 8, 8, headUv, hatUv); + head.position.y = 10; + playerGroup.add(head); + const bodyUv: UVSet = { + top: [20, 16, 8, 4], + bottom: [28, 16, 8, 4], + right: [16, 20, 4, 12], + left: [28, 20, 4, 12], + front: [20, 20, 8, 12], + back: [32, 20, 8, 12], + }; + const jacketUv: UVSet = { + top: [20, 32, 8, 4], + bottom: [28, 32, 8, 4], + right: [16, 36, 4, 12], + left: [28, 36, 4, 12], + front: [20, 36, 8, 12], + back: [32, 36, 8, 12], + }; + playerGroup.add( + createPart(texture, 8, 12, 4, bodyUv, isLegacy ? undefined : jacketUv), + ); + + const rightArm = createPart( + texture, + armW, + 12, + 4, + limbUv(40, 16, armW), + isLegacy ? undefined : limbUv(40, 32, armW), + ); + rightArm.position.set(isSlim ? -5.5 : -6, 0, 0); + playerGroup.add(rightArm); + const leftArm = createPart( + texture, + armW, + 12, + 4, + isLegacy ? limbUv(40, 16, armW) : limbUv(32, 48, armW), + isLegacy ? undefined : limbUv(48, 48, armW), + isLegacy, + isLegacy, + ); + leftArm.position.set(isSlim ? 5.5 : 6, 0, 0); + playerGroup.add(leftArm); + const rightLeg = createPart( + texture, + 4, + 12, + 4, + limbUv(0, 16), + isLegacy ? undefined : limbUv(0, 32), + ); + rightLeg.position.set(-2, -12, 0); + playerGroup.add(rightLeg); + const leftLeg = createPart( + texture, + 4, + 12, + 4, + isLegacy ? limbUv(0, 16) : limbUv(16, 48), + isLegacy ? undefined : limbUv(0, 48), + isLegacy, + isLegacy, + ); + leftLeg.position.set(2, -12, 0); + playerGroup.add(leftLeg); +} + +function addCape(playerGroup: THREE.Group, texture: THREE.Texture) { + const capeUv: UVSet = { + top: [1, 0, 10, 1], + bottom: [11, 0, 10, 1], + right: [0, 1, 1, 16], + front: [1, 1, 10, 16], + left: [11, 1, 1, 16], + back: [12, 1, 10, 16], + }; + const capeGroup = new THREE.Group(); + const capeGeo = new THREE.BoxGeometry(10, 16, 1); + const capeMats = [ + createFaceMaterial( + texture, + capeUv.left[0], + capeUv.left[1], + capeUv.left[2], + capeUv.left[3], + ), + createFaceMaterial( + texture, + capeUv.right[0], + capeUv.right[1], + capeUv.right[2], + capeUv.right[3], + ), + createFaceMaterial( + texture, + capeUv.top[0], + capeUv.top[1], + capeUv.top[2], + capeUv.top[3], + false, + true, + ), + createFaceMaterial( + texture, + capeUv.bottom[0], + capeUv.bottom[1], + capeUv.bottom[2], + capeUv.bottom[3], + false, + true, + ), + createFaceMaterial( + texture, + capeUv.back[0], + capeUv.back[1], + capeUv.back[2], + capeUv.back[3], + ), + createFaceMaterial( + texture, + capeUv.front[0], + capeUv.front[1], + capeUv.front[2], + capeUv.front[3], + ), + ]; + const capeMesh = new THREE.Mesh(capeGeo, capeMats); + capeMesh.position.set(0, -8, -0.5); + capeGroup.add(capeMesh); + capeGroup.position.set(0, 6, -2.35); + capeGroup.rotation.x = 0.15; + playerGroup.add(capeGroup); +} + +function fitCamera(camera: THREE.PerspectiveCamera, target: THREE.Object3D) { + const box = new THREE.Box3().setFromObject(target); + const center = box.getCenter(new THREE.Vector3()); + const size = box.getSize(new THREE.Vector3()); + const maxDim = Math.max(size.x, size.y, size.z) || 1; + const vFov = THREE.MathUtils.degToRad(camera.fov); + const distance = maxDim / (2 * Math.tan(vFov / 2) * 0.72); + camera.position.set(center.x, center.y, center.z + distance); + camera.lookAt(center); +} + +function addLights(scene: THREE.Scene) { + scene.add(new THREE.AmbientLight(0xffffff, 0.4)); + scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 0.6)); + const dl = new THREE.DirectionalLight(0xffffff, 0.8); + dl.position.set(10, 20, 10); + scene.add(dl); +} + +function disposeScene( + scene: THREE.Scene, + renderer: THREE.WebGLRenderer, + extraTextures: THREE.Texture[], +) { + scene.traverse((object) => { + if (object instanceof THREE.Mesh) { + if (object.geometry) object.geometry.dispose(); + const mats = Array.isArray(object.material) + ? object.material + : [object.material]; + mats.forEach((m) => { + if (m.map) m.map.dispose(); + m.dispose(); + }); + } + }); + extraTextures.forEach((t) => t.dispose()); + renderer.dispose(); +} + +function renderSkinSnapshot(opts: { + src: string; + slim?: boolean; + capeUrl?: string | null; + width: number; + height: number; +}): Promise { + return new Promise((resolve, reject) => { + const canvas = document.createElement("canvas"); + canvas.width = opts.width; + canvas.height = opts.height; + const ctx = canvas.getContext("2d"); + if (!ctx) { + reject(new Error("no 2d context")); + return; + } + const scene = new THREE.Scene(); + const camera = new THREE.PerspectiveCamera( + 35, + opts.width / opts.height, + 0.1, + 1000, + ); + const renderer = new THREE.WebGLRenderer({ + antialias: true, + alpha: true, + preserveDrawingBuffer: true, + }); + renderer.setPixelRatio(1); + renderer.setSize(opts.width, opts.height); + addLights(scene); + const playerGroup = new THREE.Group(); + scene.add(playerGroup); + const extraTextures: THREE.Texture[] = []; + let active = true; + let skinDone = false; + let capeDone = !opts.capeUrl; + const finish = () => { + if (!active || !skinDone || !capeDone) return; + fitCamera(camera, playerGroup); + renderer.render(scene, camera); + ctx.drawImage(renderer.domElement, 0, 0); + const dataUrl = canvas.toDataURL("image/png"); + disposeScene(scene, renderer, extraTextures); + active = false; + resolve(dataUrl); + }; + const fail = (err: unknown) => { + if (!active) return; + active = false; + disposeScene(scene, renderer, extraTextures); + reject(err); + }; + const loader = new THREE.TextureLoader(); + loader.load( + opts.src, + (tex) => { + if (!active) return; + tex.magFilter = THREE.NearestFilter; + tex.minFilter = THREE.NearestFilter; + tex.colorSpace = THREE.SRGBColorSpace; + buildPlayer(playerGroup, tex, opts.slim); + extraTextures.push(tex); + skinDone = true; + finish(); + }, + undefined, + fail, + ); + if (opts.capeUrl) { + loader.load( + opts.capeUrl, + (tex) => { + if (!active) return; + tex.magFilter = THREE.NearestFilter; + tex.minFilter = THREE.NearestFilter; + tex.colorSpace = THREE.SRGBColorSpace; + addCape(playerGroup, tex); + extraTextures.push(tex); + capeDone = true; + finish(); + }, + undefined, + fail, + ); + } + }); +} + +export function getSkinModelSnapshot(opts: { + src: string; + slim?: boolean; + capeUrl?: string | null; + width: number; + height: number; +}): Promise { + const key = `${opts.src}\u0000${opts.slim ? 1 : 0}\u0000${opts.capeUrl || ""}`; + const hit = snapshotCache.get(key); + if (hit) return Promise.resolve(hit); + const result = snapshotChain.then(() => renderSkinSnapshot(opts)); + snapshotChain = result.then( + () => undefined, + () => undefined, + ); + result.then((dataUrl) => snapshotCache.set(key, dataUrl)).catch(() => {}); + return result; +} + +const SkinModel3D = memo(function SkinModel3D({ + src, + slim, + capeUrl, + animate, + className, +}: SkinModel3DProps) { + const mountRef = useRef(null); + + useEffect(() => { + const el = mountRef.current; + if (!el) return; + const width = el.clientWidth || 240; + const height = el.clientHeight || 320; + const scene = new THREE.Scene(); + const camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 1000); + const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); + renderer.setSize(width, height); + el.innerHTML = ""; + el.appendChild(renderer.domElement); + addLights(scene); + const playerGroup = new THREE.Group(); + scene.add(playerGroup); + const extraTextures: THREE.Texture[] = []; + let built = false; + let disposed = false; + let rotY = -0.35; + let rotX = 0.06; + let dragging = false; + let raf = 0; + let lastX = 0; + let lastY = 0; + + const render = () => { + if (!disposed) renderer.render(scene, camera); + }; + + const onMouseDown = (e: MouseEvent) => { + dragging = true; + lastX = e.clientX; + lastY = e.clientY; + }; + const onMouseMove = (e: MouseEvent) => { + if (!dragging || disposed) return; + rotY += (e.clientX - lastX) * 0.01; + rotX = clamp(rotX + (e.clientY - lastY) * 0.01, -0.6, 0.6); + lastX = e.clientX; + lastY = e.clientY; + }; + const onMouseUp = () => { + dragging = false; + }; + const onTouchStart = (e: TouchEvent) => { + const t = e.touches[0]; + if (!t) return; + dragging = true; + lastX = t.clientX; + lastY = t.clientY; + }; + const onTouchMove = (e: TouchEvent) => { + if (!dragging || disposed) return; + const t = e.touches[0]; + if (!t) return; + rotY += (t.clientX - lastX) * 0.01; + rotX = clamp(rotX + (t.clientY - lastY) * 0.01, -0.6, 0.6); + lastX = t.clientX; + lastY = t.clientY; + }; + const onTouchEnd = () => { + dragging = false; + }; + + const loop = () => { + raf = requestAnimationFrame(loop); + if (animate && !dragging) rotY += 0.004; + if (animate || dragging) { + playerGroup.rotation.y = rotY; + playerGroup.rotation.x = rotX; + render(); + } + }; + + const handleResize = () => { + if (!mountRef.current || disposed) return; + const w = mountRef.current.clientWidth || width; + const h = mountRef.current.clientHeight || height; + camera.aspect = w / h; + camera.updateProjectionMatrix(); + renderer.setSize(w, h); + if (built) fitCamera(camera, playerGroup); + render(); + }; + + const dispose = () => { + if (disposed) return; + disposed = true; + cancelAnimationFrame(raf); + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseup", onMouseUp); + window.removeEventListener("touchmove", onTouchMove); + window.removeEventListener("touchend", onTouchEnd); + window.removeEventListener("resize", handleResize); + disposeScene(scene, renderer, extraTextures); + }; + + const loader = new THREE.TextureLoader(); + let skinLoaded = false; + let capeLoaded = !capeUrl; + const tryFinish = () => { + if (!skinLoaded || !capeLoaded || disposed) return; + built = true; + fitCamera(camera, playerGroup); + render(); + loop(); + }; + loader.load( + src, + (tex) => { + if (disposed) return; + tex.magFilter = THREE.NearestFilter; + tex.minFilter = THREE.NearestFilter; + tex.colorSpace = THREE.SRGBColorSpace; + buildPlayer(playerGroup, tex, slim); + extraTextures.push(tex); + skinLoaded = true; + tryFinish(); + }, + undefined, + () => dispose(), + ); + if (capeUrl) { + loader.load( + capeUrl, + (tex) => { + if (disposed) return; + tex.magFilter = THREE.NearestFilter; + tex.minFilter = THREE.NearestFilter; + tex.colorSpace = THREE.SRGBColorSpace; + addCape(playerGroup, tex); + extraTextures.push(tex); + capeLoaded = true; + tryFinish(); + }, + undefined, + () => dispose(), + ); + } + + renderer.domElement.addEventListener("mousedown", onMouseDown); + window.addEventListener("mousemove", onMouseMove); + window.addEventListener("mouseup", onMouseUp); + renderer.domElement.addEventListener("touchstart", onTouchStart, { + passive: true, + }); + window.addEventListener("touchmove", onTouchMove, { passive: false }); + window.addEventListener("touchend", onTouchEnd); + window.addEventListener("resize", handleResize); + + return dispose; + }, [src, slim, capeUrl, animate]); + + return
; +}); + +export default SkinModel3D; diff --git a/src/components/views/SkinsView.tsx b/src/components/views/SkinsView.tsx index cc939b1..ef5de33 100644 --- a/src/components/views/SkinsView.tsx +++ b/src/components/views/SkinsView.tsx @@ -9,7 +9,7 @@ import { useSkin, useConfig, } from "../../context/LauncherContext"; -import SkinViewer from "../common/SkinViewer"; +import SkinModel3D, { getSkinModelSnapshot } from "../common/SkinModel3D"; import CapePreview from "../common/CapePreview"; import { usePlatform } from "../../hooks/usePlatform"; @@ -71,7 +71,7 @@ const DEFAULT_SKINS: SavedSkin[] = [ url: "/Skins/PrismaChunk0.png", isSlim: false, }, - { id: "amy", name: "Amy", url: "/Skins/amy.png", isSlim: true }, //neo: :c + { id: "amy", name: "Amy", url: "/Skins/amy.png", isSlim: true }, { id: "avalilac", name: "AvaLilac", url: "/Skins/ava.png", isSlim: true }, { id: "huckle", name: "Huckle", url: "/Skins/huckle.png", isSlim: true }, { @@ -86,6 +86,18 @@ const DEFAULT_SKINS: SavedSkin[] = [ url: "/Skins/tranq.png", isSlim: true, }, + { + id: "individul", + name: "individul", + url: "/Skins/individul.png", + isSlim: false, + }, + { + id: "ency", + name: "ency_pt", + url: "/Skins/ency.png", + isSlim: true, + }, ]; const DEFAULT_CAPES: SavedCape[] = [ @@ -93,57 +105,63 @@ const DEFAULT_CAPES: SavedCape[] = [ { id: "unused2", name: "Unused Cape 2", url: "/Capes/Unused_Cape_2.png" }, ]; -const HeadPreview = memo(function HeadPreview({ src }: { src: string }) { - const canvasRef = useRef(null); - +const SkinCardModel = memo(function SkinCardModel({ + src, + slim, +}: { + src: string; + slim?: boolean; +}) { + const boxRef = useRef(null); + const [snapshot, setSnapshot] = useState(null); useEffect(() => { - const cvs = canvasRef.current; - if (!cvs) return; - const ctx = cvs.getContext("2d"); - if (!ctx) return; - const img = new Image(); - img.onload = () => { - ctx.clearRect(0, 0, cvs.width, cvs.height); - ctx.imageSmoothingEnabled = false; - ctx.drawImage(img, 8, 8, 8, 8, 0, 0, cvs.width, cvs.height); - if (img.height !== 32) { - ctx.drawImage(img, 40, 8, 8, 8, 0, 0, cvs.width, cvs.height); - } + let active = true; + const el = boxRef.current; + if (!el) return; + const w = Math.max(48, Math.round(el.clientWidth * 2)); + const h = Math.max(64, Math.round(el.clientHeight * 2)); + getSkinModelSnapshot({ src, slim, width: w, height: h }) + .then((dataUrl) => { + if (active) setSnapshot(dataUrl); + }) + .catch(() => {}); + return () => { + active = false; }; - img.src = src; - }, [src]); + }, [src, slim]); return ( - +
+ {snapshot ? ( + + ) : ( +
+ + ... + +
+ )} +
); }); const SkinsView = memo(function SkinsView() { const { t } = useTranslation(); - const { setActiveView, setIsUiHidden } = useUI(); + const { setActiveView } = useUI(); const { playPressSound, playBackSound } = useAudio(); const { isAndroid } = usePlatform(); - const { - skinUrl, - setSkinUrl, - skinIsSlim, - setSkinIsSlim, - capeUrl, - setCapeUrl, - } = useSkin(); - + const config = useConfig(); + const { skinUrl, setSkinUrl, setSkinIsSlim, capeUrl, setCapeUrl } = useSkin(); const [focusIndex, setFocusIndex] = useState(null); const [viewMode, setViewMode] = useState<"skin" | "cape">("skin"); const containerRef = useRef(null); const fileInputRef = useRef(null); const capeFileInputRef = useRef(null); - const [storedSkins, setStoredSkins] = useLocalStorage( "lce-custom-skins", [], @@ -534,102 +552,102 @@ const SkinsView = memo(function SkinsView() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - transition={{ duration: useConfig().animationsEnabled ? 0.3 : 0 }} - className="flex flex-col items-center w-full max-w-3xl h-full outline-none" + transition={{ duration: config.animationsEnabled ? 0.3 : 0 }} + className="flex flex-col items-center w-full max-w-5xl h-full outline-none" > -

+

{viewMode === "skin" ? t("skins.skinLibrary") : t("skins.capeLibrary")}

-
-
- - - - - {viewMode === "skin" && ( +
+
+
- )} -
-
+ + + {viewMode === "skin" && ( + + )} + +
+ +
- - -
- -
- {viewMode === "skin" ? ( - savedSkins.map((skin, i) => { - const idx = SKINS_START_INDEX + i; - const isActive = activeSkinId - ? activeSkinId === skin.id - : skinUrl === skin.url; - const isFocused = focusIndex === idx; - return ( -
setFocusIndex(idx)} - className="flex flex-col items-center gap-1 w-32 outline-none" - > -
- {isActive && ( - - {t("skins.active")} - - )} - {skin.isSlim && ( - - {t("skins.slim")} - - )} -
+
+
+ {viewMode === "skin" ? ( + savedSkins.map((skin, i) => { + const idx = SKINS_START_INDEX + i; + const isActive = activeSkinId + ? activeSkinId === skin.id + : skinUrl === skin.url; + const isFocused = focusIndex === idx; + const isHighlight = isActive || isFocused; + return ( +
setFocusIndex(idx)} + className="flex flex-col items-center gap-1.5 outline-none" + > +
handleSkinSelect(skin)} + className={`relative w-full bg-black/30 border-2 overflow-hidden transition-colors outline-none cursor-pointer flex items-center justify-center ${isHighlight ? "border-[#FFFF55]" : "border-[#373737] hover:border-[#A0A0A0]"}`} + style={{ aspectRatio: "3 / 4" }} + > + + {isHighlight && ( +
+ )} +
+
+ + handleNameChange(skin.id, e.target.value) + } + className={`mc-textinput w-full h-9 px-2 text-sm text-center truncate outline-none relative z-10 font-[var(--font-base)] ${isHighlight ? "text-[#FFFF55]" : "text-white"} ${isDefaultSkin(skin.id) ? "pointer-events-none" : ""}`} + onClick={(e) => e.stopPropagation()} + spellCheck={false} + readOnly={isDefaultSkin(skin.id)} + /> +
+
+ ); + }) + ) : ( + <>
handleSkinSelect(skin)} - className={`w-16 h-16 bg-black/40 border-2 shadow-inner relative cursor-pointer overflow-hidden transition-colors outline-none ${isActive || isFocused ? "border-[#FFFF55]" : "border-[#373737] hover:border-[#A0A0A0]"}`} - > - -
-
- - handleNameChange(skin.id, e.target.value) - } - className={`w-full h-10 px-3 text-base text-center truncate outline-none relative z-10 font-[var(--font-base)] ${isActive || isFocused ? "text-[#FFFF55]" : "text-white"} ${isDefaultSkin(skin.id) ? "pointer-events-none" : ""}`} - onClick={(e) => e.stopPropagation()} - spellCheck={false} - readOnly={isDefaultSkin(skin.id)} - /> -
-
- ); - }) - ) : ( - <> -
setFocusIndex(SKINS_START_INDEX)} - className="flex flex-col items-center gap-1 w-32 outline-none" - > -
- {isActiveCapeDefault && ( - - Active - - )} -
-
{ - playPressSound(); - setCapeUrl(null); - setActiveCapeId(null); - }} - className={`w-16 h-16 bg-black/40 border-2 shadow-inner relative cursor-pointer overflow-hidden transition-colors outline-none flex items-center justify-center ${isActiveCapeDefault || focusIndex === SKINS_START_INDEX ? "border-[#FFFF55]" : "border-[#373737] hover:border-[#A0A0A0]"}`} - > - X -
- - {t("skins.noCape")} - -
- {savedCapes.map((cape, i) => { - const idx = SKINS_START_INDEX + 1 + i; - const isActive = activeCapeId - ? activeCapeId === cape.id - : capeUrl === cape.url; - const isFocused = focusIndex === idx; - return ( -
setFocusIndex(idx)} - className="flex flex-col items-center gap-1 w-32 outline-none" + onMouseEnter={() => setFocusIndex(SKINS_START_INDEX)} + className="flex flex-col items-center gap-1.5 outline-none" > -
- {isActive && ( - - Active - +
{ + playPressSound(); + setCapeUrl(null); + setActiveCapeId(null); + }} + className={`relative w-full bg-black/30 border-2 overflow-hidden transition-colors outline-none cursor-pointer flex items-center justify-center ${isActiveCapeDefault || focusIndex === SKINS_START_INDEX ? "border-[#FFFF55]" : "border-[#373737] hover:border-[#A0A0A0]"}`} + style={{ aspectRatio: "3 / 4" }} + > + + X + + {(isActiveCapeDefault || + focusIndex === SKINS_START_INDEX) && ( +
)}
-
handleCapeSelect(cape)} - className={`w-16 h-16 bg-black/40 border-2 shadow-inner relative cursor-pointer overflow-hidden transition-colors outline-none ${isActive || isFocused ? "border-[#FFFF55]" : "border-[#373737] hover:border-[#A0A0A0]"}`} + - -
-
- - handleCapeNameChange(cape.id, e.target.value) - } - className={`mc-textinput w-full h-10 px-3 text-base text-center truncate outline-none relative z-10 font-[var(--font-base)] ${isActive || isFocused ? "text-[#FFFF55]" : "text-white"} ${isDefaultCape(cape.id) ? "pointer-events-none" : ""}`} - onClick={(e) => e.stopPropagation()} - spellCheck={false} - readOnly={isDefaultCape(cape.id)} - /> -
+ {t("skins.noCape")} +
- ); - })} - - )} + {savedCapes.map((cape, i) => { + const idx = SKINS_START_INDEX + 1 + i; + const isActive = activeCapeId + ? activeCapeId === cape.id + : capeUrl === cape.url; + const isFocused = focusIndex === idx; + const isHighlight = isActive || isFocused; + return ( +
setFocusIndex(idx)} + className="flex flex-col items-center gap-1.5 outline-none" + > +
handleCapeSelect(cape)} + className={`relative w-full bg-black/30 border-2 overflow-hidden transition-colors outline-none cursor-pointer ${isHighlight ? "border-[#FFFF55]" : "border-[#373737] hover:border-[#A0A0A0]"}`} + style={{ aspectRatio: "3 / 4" }} + > + + {isHighlight && ( +
+ )} +
+
+ + handleCapeNameChange(cape.id, e.target.value) + } + className={`mc-textinput w-full h-9 px-2 text-sm text-center truncate outline-none relative z-10 font-[var(--font-base)] ${isHighlight ? "text-[#FFFF55]" : "text-white"} ${isDefaultCape(cape.id) ? "pointer-events-none" : ""}`} + onClick={(e) => e.stopPropagation()} + spellCheck={false} + readOnly={isDefaultCape(cape.id)} + /> +
+
+ ); + })} + + )} +
+
-
- {}} - playPressSound={playPressSound} - skinUrl={skinUrl} - setSkinUrl={setSkinUrl} - capeUrl={capeUrl} - setActiveView={setActiveView} - setIsUiHidden={setIsUiHidden} - isFocusedSection={false} - onNavigateRight={() => {}} - hideControls - style={{ top: "45%" }} - slim={skinIsSlim} - /> -
- {!isAndroid && (