diff --git a/src/components/common/SkinViewer.tsx b/src/components/common/SkinViewer.tsx index a47f833..7347ed3 100644 --- a/src/components/common/SkinViewer.tsx +++ b/src/components/common/SkinViewer.tsx @@ -1,10 +1,8 @@ -import { useEffect, useRef, useState, memo } from 'react'; -import { motion } from 'framer-motion'; -import * as THREE from 'three'; -import { useConfig } from '../../context/LauncherContext'; - +import { useEffect, useRef, useState, memo } from "react"; +import { motion } from "framer-motion"; +import * as THREE from "three"; +import { useConfig } from "../../context/LauncherContext"; type UVSet = Record; - interface SkinViewerProps { username: string; setUsername: (name: string) => void; @@ -16,9 +14,21 @@ interface SkinViewerProps { isFocusedSection: boolean; onNavigateRight: () => void; hideControls?: boolean; + style?: React.CSSProperties; } -const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSound, skinUrl, capeUrl, setActiveView, isFocusedSection, onNavigateRight, hideControls }: SkinViewerProps) { +const SkinViewer = memo(function SkinViewer({ + username, + setUsername, + playPressSound, + skinUrl, + capeUrl, + setActiveView, + isFocusedSection, + onNavigateRight, + hideControls, + style, +}: SkinViewerProps) { const mountRef = useRef(null); const containerRef = useRef(null); const [focusIndex, setFocusIndex] = useState(0); @@ -26,7 +36,6 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo const overlaysRef = useRef([]); const capeRef = useRef(null); const requestRenderRef = useRef<(() => void) | null>(null); - useEffect(() => { if (!mountRef.current) return; const width = 260; @@ -54,26 +63,91 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo texture.colorSpace = THREE.SRGBColorSpace; const img = texture.image; const isLegacy = img.height === 32; - const createFaceMaterial = (x: number, y: number, w: number, h: number, flipX = false, flipY = false, tex = texture) => { + const createFaceMaterial = ( + x: number, + y: number, + w: number, + h: number, + flipX = false, + flipY = false, + tex = texture, + ) => { const matTex = tex.clone(); - matTex.repeat.set((flipX ? -w : w) / 64, (flipY ? -h : h) / tex.image.height); - matTex.offset.set((flipX ? (x + w) : x) / 64, 1 - (flipY ? y : (y + h)) / tex.image.height); + matTex.repeat.set( + (flipX ? -w : w) / 64, + (flipY ? -h : h) / tex.image.height, + ); + matTex.offset.set( + (flipX ? x + w : x) / 64, + 1 - (flipY ? y : y + h) / tex.image.height, + ); matTex.needsUpdate = true; - return new THREE.MeshLambertMaterial({ map: matTex, transparent: true, alphaTest: 0.5, side: THREE.FrontSide }); + return new THREE.MeshLambertMaterial({ + map: matTex, + transparent: true, + alphaTest: 0.5, + side: THREE.FrontSide, + }); }; - const createPart = (w: number, h: number, d: number, uv: UVSet, overlayUv?: UVSet, swapMats = false, isLegacyMirror = false) => { + const createPart = ( + 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(swapMats ? uvSet.right[0] : uvSet.left[0], uvSet.left[1], uvSet.left[2], uvSet.left[3], flipX), - createFaceMaterial(swapMats ? uvSet.left[0] : uvSet.right[0], uvSet.right[1], uvSet.right[2], uvSet.right[3], flipX), - createFaceMaterial(uvSet.top[0], uvSet.top[1], uvSet.top[2], uvSet.top[3], flipX, true), - createFaceMaterial(uvSet.bottom[0], uvSet.bottom[1], uvSet.bottom[2], uvSet.bottom[3], flipX, true), - createFaceMaterial(uvSet.front[0], uvSet.front[1], uvSet.front[2], uvSet.front[3], flipX), - createFaceMaterial(uvSet.back[0], uvSet.back[1], uvSet.back[2], uvSet.back[3], !flipX) + createFaceMaterial( + swapMats ? uvSet.right[0] : uvSet.left[0], + uvSet.left[1], + uvSet.left[2], + uvSet.left[3], + flipX, + ), + createFaceMaterial( + swapMats ? uvSet.left[0] : uvSet.right[0], + uvSet.right[1], + uvSet.right[2], + uvSet.right[3], + flipX, + ), + createFaceMaterial( + uvSet.top[0], + uvSet.top[1], + uvSet.top[2], + uvSet.top[3], + flipX, + true, + ), + createFaceMaterial( + uvSet.bottom[0], + uvSet.bottom[1], + uvSet.bottom[2], + uvSet.bottom[3], + flipX, + true, + ), + createFaceMaterial( + uvSet.front[0], + uvSet.front[1], + uvSet.front[2], + uvSet.front[3], + flipX, + ), + createFaceMaterial( + uvSet.back[0], + uvSet.back[1], + uvSet.back[2], + uvSet.back[3], + !flipX, + ), ]; }; @@ -90,39 +164,103 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo }; const limbUv = (x: number, y: number, w = 4) => ({ - 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] + 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], }); - const isSlim = !isLegacy && (() => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) return false; - ctx.drawImage(img, 0, 0); - const data = ctx.getImageData(42, 48, 1, 1).data; - return data[3] === 0; - })(); + const isSlim = + !isLegacy && + (() => { + const canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext("2d"); + if (!ctx) return false; + ctx.drawImage(img, 0, 0); + const data = ctx.getImageData(42, 48, 1, 1).data; + return data[3] === 0; + })(); const armW = isSlim ? 3 : 4; - const headUv = { 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 = { 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 headUv = { + 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 = { + 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(8, 8, 8, headUv, hatUv); head.position.y = 10; playerGroup.add(head); - const bodyUv = { 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 = { 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(8, 12, 4, bodyUv, isLegacy ? undefined : jacketUv)); - const rightArm = createPart(armW, 12, 4, limbUv(40, 16, armW), isLegacy ? undefined : limbUv(40, 32, armW)); + const bodyUv = { + 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 = { + 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(8, 12, 4, bodyUv, isLegacy ? undefined : jacketUv), + ); + const rightArm = createPart( + 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(armW, 12, 4, isLegacy ? limbUv(40, 16, armW) : limbUv(32, 48, armW), isLegacy ? undefined : limbUv(48, 48, armW), true, isLegacy); + const leftArm = createPart( + armW, + 12, + 4, + isLegacy ? limbUv(40, 16, armW) : limbUv(32, 48, armW), + isLegacy ? undefined : limbUv(48, 48, armW), + true, + isLegacy, + ); leftArm.position.set(isSlim ? 5.5 : 6, 0, 0); playerGroup.add(leftArm); - const rightLeg = createPart(4, 12, 4, limbUv(0, 16), isLegacy ? undefined : limbUv(0, 32)); + const rightLeg = createPart( + 4, + 12, + 4, + limbUv(0, 16), + isLegacy ? undefined : limbUv(0, 32), + ); rightLeg.position.set(-2, -12, 0); playerGroup.add(rightLeg); - const leftLeg = createPart(4, 12, 4, isLegacy ? limbUv(0, 16) : limbUv(16, 48), isLegacy ? undefined : limbUv(0, 48), true, isLegacy); + const leftLeg = createPart( + 4, + 12, + 4, + isLegacy ? limbUv(0, 16) : limbUv(16, 48), + isLegacy ? undefined : limbUv(0, 48), + true, + isLegacy, + ); leftLeg.position.set(2, -12, 0); playerGroup.add(leftLeg); if (capeUrl) { @@ -131,20 +269,71 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo capeTexture.minFilter = THREE.NearestFilter; capeTexture.colorSpace = THREE.SRGBColorSpace; const capeUv = { - 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] + 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(capeUv.left[0], capeUv.left[1], capeUv.left[2], capeUv.left[3], false, false, capeTexture), - createFaceMaterial(capeUv.right[0], capeUv.right[1], capeUv.right[2], capeUv.right[3], false, false, capeTexture), - createFaceMaterial(capeUv.top[0], capeUv.top[1], capeUv.top[2], capeUv.top[3], false, true, capeTexture), - createFaceMaterial(capeUv.bottom[0], capeUv.bottom[1], capeUv.bottom[2], capeUv.bottom[3], false, true, capeTexture), - createFaceMaterial(capeUv.back[0], capeUv.back[1], capeUv.back[2], capeUv.back[3], false, false, capeTexture), - createFaceMaterial(capeUv.front[0], capeUv.front[1], capeUv.front[2], capeUv.front[3], false, false, capeTexture) + createFaceMaterial( + capeUv.left[0], + capeUv.left[1], + capeUv.left[2], + capeUv.left[3], + false, + false, + capeTexture, + ), + createFaceMaterial( + capeUv.right[0], + capeUv.right[1], + capeUv.right[2], + capeUv.right[3], + false, + false, + capeTexture, + ), + createFaceMaterial( + capeUv.top[0], + capeUv.top[1], + capeUv.top[2], + capeUv.top[3], + false, + true, + capeTexture, + ), + createFaceMaterial( + capeUv.bottom[0], + capeUv.bottom[1], + capeUv.bottom[2], + capeUv.bottom[3], + false, + true, + capeTexture, + ), + createFaceMaterial( + capeUv.back[0], + capeUv.back[1], + capeUv.back[2], + capeUv.back[3], + false, + false, + capeTexture, + ), + createFaceMaterial( + capeUv.front[0], + capeUv.front[1], + capeUv.front[2], + capeUv.front[3], + false, + false, + capeTexture, + ), ]; const capeMesh = new THREE.Mesh(capeGeo, capeMats); @@ -167,7 +356,9 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo isDragging = true; previousMousePosition = { x: e.clientX, y: e.clientY }; }; - const onMouseUp = () => { isDragging = false; }; + const onMouseUp = () => { + isDragging = false; + }; const onMouseMove = (e: MouseEvent) => { if (isDragging) { const delta = (e.clientX - previousMousePosition.x) * 0.01; @@ -215,43 +406,55 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo } const handleKeyDown = (e: KeyboardEvent) => { - if (document.activeElement?.tagName === 'INPUT') return; - - if (e.key === 'ArrowRight') { + if (document.activeElement?.tagName === "INPUT") return; + if (e.key === "ArrowRight") { if (legacyMode) onNavigateRight(); else if (focusIndex === 3) onNavigateRight(); - else if (focusIndex >= 1 && focusIndex < 3) setFocusIndex(prev => prev + 1); - } else if (e.key === 'ArrowLeft') { + else if (focusIndex >= 1 && focusIndex < 3) + setFocusIndex((prev) => prev + 1); + } else if (e.key === "ArrowLeft") { if (legacyMode) return; - if (focusIndex > 1 && focusIndex <= 3) setFocusIndex(prev => prev - 1); - } else if (e.key === 'ArrowDown') { - setFocusIndex(prev => (prev < 3 ? prev + 1 : prev)); - } else if (e.key === 'ArrowUp') { - setFocusIndex(prev => (prev > 0 ? prev - 1 : prev)); - } else if (e.key === 'Enter') { + if (focusIndex > 1 && focusIndex <= 3) + setFocusIndex((prev) => prev - 1); + } else if (e.key === "ArrowDown") { + setFocusIndex((prev) => (prev < 3 ? prev + 1 : prev)); + } else if (e.key === "ArrowUp") { + setFocusIndex((prev) => (prev > 0 ? prev - 1 : prev)); + } else if (e.key === "Enter") { if (focusIndex === 0) { - (containerRef.current?.querySelector('input') as HTMLElement)?.focus(); + ( + containerRef.current?.querySelector("input") as HTMLElement + )?.focus(); } else if (focusIndex === 1) { playPressSound(); - setActiveView('skins'); + setActiveView("skins"); } else if (focusIndex === 2) { playPressSound(); - setActiveView('screenshots'); + setActiveView("screenshots"); } else if (focusIndex === 3) { playPressSound(); - setActiveView('lcelive'); + setActiveView("lcelive"); } } }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [isFocusedSection, focusIndex, onNavigateRight, playPressSound, setActiveView, legacyMode]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [ + isFocusedSection, + focusIndex, + onNavigateRight, + playPressSound, + setActiveView, + legacyMode, + ]); useEffect(() => { if (isFocusedSection) { - const el = containerRef.current?.querySelector(`[data-focus="${focusIndex}"]`) as HTMLElement; - if (el && document.activeElement?.tagName !== 'INPUT') el.focus(); + const el = containerRef.current?.querySelector( + `[data-focus="${focusIndex}"]`, + ) as HTMLElement; + if (el && document.activeElement?.tagName !== "INPUT") el.focus(); } }, [isFocusedSection, focusIndex]); @@ -262,16 +465,23 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} transition={{ duration: useConfig().animationsEnabled ? 0.3 : 0 }} - className={`absolute ${legacyMode ? 'left-[calc(50vw-340px)]' : 'left-16'} ${legacyMode ? 'top-1/2' : 'top-[40%]'} -translate-y-1/2 flex flex-col items-center gap-1 outline-none z-10`} + className={`absolute ${legacyMode ? "left-[calc(50vw-340px)]" : "left-16"} ${legacyMode ? "top-1/2" : "top-[40%]"} -translate-y-1/2 flex flex-col items-center gap-1 outline-none z-10`} + style={style} > {!hideControls && !legacyMode && ( -
+
setUsername(e.target.value)} onKeyDown={(e) => { - if (e.key === 'Enter') { + if (e.key === "Enter") { e.currentTarget.blur(); e.stopPropagation(); } @@ -282,44 +492,96 @@ const SkinViewer = memo(function SkinViewer({ username, setUsername, playPressSo )} {!legacyMode && (
-
+
)} {!hideControls && ( -
+
)} - ); }); diff --git a/src/components/views/SkinsView.tsx b/src/components/views/SkinsView.tsx index 9c97c61..79e089d 100644 --- a/src/components/views/SkinsView.tsx +++ b/src/components/views/SkinsView.tsx @@ -715,6 +715,7 @@ const SkinsView = memo(function SkinsView() { isFocusedSection={false} onNavigateRight={() => {}} hideControls + style={{ top: "45%" }} />