1079 lines
42 KiB
JavaScript
1079 lines
42 KiB
JavaScript
var AssetsCurrentlyEquipped = []
|
|
var AssetTypeDict = {}
|
|
var AvatarRules = {}
|
|
var UserBodyColors = {
|
|
'Head': 1001,
|
|
'Torso': 1001,
|
|
'LeftArm': 1001,
|
|
'RightArm': 1001,
|
|
'LeftLeg': 1001,
|
|
'RightLeg': 1001
|
|
}
|
|
var UserAvatarScaling = {
|
|
'height': 1,
|
|
'width': 1,
|
|
'head': 1,
|
|
'proportion': 0
|
|
}
|
|
var rigType = "R6"
|
|
var AssetCardTemplate = null
|
|
var PageNumber = 0
|
|
var SelectedAssetType = 11
|
|
var IsThisTheLastPage = false
|
|
var PaginationPreviousBtn = null
|
|
var PaginationNextBtn = null
|
|
var PaginationText = null
|
|
|
|
var is3DViewEnabled = localStorage.getItem('avatar3DView') === 'false';
|
|
var current3DHash = null;
|
|
|
|
async function GetLimitForAssetType( AssetType ) {
|
|
// Loop through the "wearableAssetTypes" array in avatarrules.json
|
|
for (let i = 0; i < AvatarRules.wearableAssetTypes.length; i++) {
|
|
if (AvatarRules.wearableAssetTypes[i].id == AssetType) {
|
|
return AvatarRules.wearableAssetTypes[i].maxNumber
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
async function GetAssetsForType( AssetType, Page = 0 ) {
|
|
var AssetFetchResponse = await fetch('/avatar/getassets?type=' + AssetType + '&page=' + Page)
|
|
if (AssetFetchResponse.status != 200) {
|
|
return []
|
|
}
|
|
var AssetFetchResponseJSON = await AssetFetchResponse.json()
|
|
var Assets = AssetFetchResponseJSON["assets"]
|
|
// Insert the assets into the AssetTypeDict
|
|
for (let i = 0; i < Assets.length; i++) {
|
|
Assets[i].assetType = AssetType
|
|
AssetTypeDict[Assets[i].id] = Assets[i]
|
|
}
|
|
IsThisTheLastPage = AssetFetchResponseJSON["lastPage"]
|
|
return Assets
|
|
}
|
|
|
|
|
|
async function toggle3DView() {
|
|
is3DViewEnabled = !is3DViewEnabled;
|
|
localStorage.setItem('avatar3DView', is3DViewEnabled.toString());
|
|
document.getElementById('toggle-3d-btn').innerText = is3DViewEnabled ? '2D' : '3D';
|
|
|
|
if (is3DViewEnabled) {
|
|
await checkAndRender3DAvatar();
|
|
} else {
|
|
show2DAvatar();
|
|
}
|
|
}
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.loading-indicator {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background-color: rgba(255, 255, 255, 0.8);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.spinner-container {
|
|
width: 40px;
|
|
height: 40px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.spinner {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: 4px solid rgba(0, 0, 0, 0.1);
|
|
border-radius: 50%;
|
|
border-top-color: #3498db;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
.loading-text {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #333;
|
|
}
|
|
|
|
.error-indicator {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
background-color: rgba(255, 255, 255, 0.9);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.error-indicator span {
|
|
font-size: 24px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
});
|
|
function checkAndRender3DAvatar() {
|
|
const avatarImage = document.getElementById('avatar-image');
|
|
const avatarContainer = document.querySelector('.avatar-image-container');
|
|
|
|
if (!avatarContainer) {
|
|
console.error('Avatar container not found');
|
|
return;
|
|
}
|
|
|
|
if (avatarImage) {
|
|
avatarImage.style.opacity = 0;
|
|
}
|
|
|
|
try {
|
|
const initialLoadingIndicator = document.createElement('div');
|
|
initialLoadingIndicator.id = 'initial-3d-loading';
|
|
initialLoadingIndicator.className = 'loading-indicator';
|
|
initialLoadingIndicator.innerHTML = `
|
|
<div class="spinner-container">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
<div class="loading-text">Initializing 3D View...</div>
|
|
`;
|
|
initialLoadingIndicator.style.position = 'absolute';
|
|
initialLoadingIndicator.style.top = '50%';
|
|
initialLoadingIndicator.style.left = '50%';
|
|
initialLoadingIndicator.style.transform = 'translate(-50%, -50%)';
|
|
initialLoadingIndicator.style.textAlign = 'center';
|
|
initialLoadingIndicator.style.zIndex = '10';
|
|
avatarContainer.appendChild(initialLoadingIndicator);
|
|
|
|
fetch('/avatar/3d')
|
|
.then(response => {
|
|
if (!response.ok) throw new Error('Failed to fetch 3D avatar data');
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
const loadingIndicator = document.getElementById('initial-3d-loading');
|
|
if (loadingIndicator) loadingIndicator.remove();
|
|
|
|
if (data.state === 'Completed' && data.imageUrl) {
|
|
return fetch(`/cdn_local/${data.imageUrl}`)
|
|
.then(modelResponse => {
|
|
if (!modelResponse.ok) throw new Error('Failed to fetch 3D model config');
|
|
return modelResponse.json();
|
|
})
|
|
.then(modelData => {
|
|
current3DHash = data.imageUrl;
|
|
render3DAvatar(modelData);
|
|
});
|
|
} else if (data.state === 'Pending') {
|
|
pollFor3DAvatar();
|
|
} else {
|
|
throw new Error('Invalid 3D avatar state');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error in 3D avatar rendering:', error);
|
|
|
|
const loadingIndicator = document.getElementById('initial-3d-loading');
|
|
if (loadingIndicator) loadingIndicator.remove();
|
|
|
|
show2DAvatar();
|
|
|
|
const errorIcon = document.createElement('div');
|
|
errorIcon.className = 'error-indicator';
|
|
errorIcon.innerHTML = '<span class="icon-spot-error-2xl"></span><div>Failed to initialize 3D view</div>';
|
|
errorIcon.style.position = 'absolute';
|
|
errorIcon.style.top = '50%';
|
|
errorIcon.style.left = '50%';
|
|
errorIcon.style.transform = 'translate(-50%, -50%)';
|
|
errorIcon.style.textAlign = 'center';
|
|
errorIcon.style.color = '#ff3333';
|
|
avatarContainer.appendChild(errorIcon);
|
|
|
|
setTimeout(() => {
|
|
if (errorIcon && errorIcon.parentNode) {
|
|
errorIcon.remove();
|
|
}
|
|
}, 3000);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error in 3D avatar initialization:', error);
|
|
show2DAvatar();
|
|
}
|
|
}
|
|
|
|
function tween(start, end, duration, onUpdate, onComplete) {
|
|
let startTime = Date.now();
|
|
function update() {
|
|
let now = Date.now();
|
|
let progress = Math.min((now - startTime) / duration, 1);
|
|
let value = start + (end - start) * progress;
|
|
onUpdate(value);
|
|
if (progress < 1) {
|
|
requestAnimationFrame(update);
|
|
} else {
|
|
onComplete();
|
|
}
|
|
}
|
|
update();
|
|
}
|
|
|
|
async function pollFor3DAvatar() {
|
|
const maxAttempts = 30;
|
|
let attempts = 0;
|
|
|
|
const avatarImage = document.getElementById('avatar-image');
|
|
const avatarContainer = document.querySelector('.avatar-image-container');
|
|
|
|
if (!avatarContainer) {
|
|
console.error('Avatar container not found');
|
|
return;
|
|
}
|
|
|
|
const loadingIndicator = document.createElement('div');
|
|
loadingIndicator.id = '3d-avatar-spinner';
|
|
loadingIndicator.className = 'loading-indicator';
|
|
loadingIndicator.innerHTML = `
|
|
<div class="spinner-container">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
<div class="loading-text">Loading 3D Avatar...</div>
|
|
`;
|
|
loadingIndicator.style.position = 'absolute';
|
|
loadingIndicator.style.top = '50%';
|
|
loadingIndicator.style.left = '50%';
|
|
loadingIndicator.style.transform = 'translate(-50%, -50%)';
|
|
loadingIndicator.style.textAlign = 'center';
|
|
loadingIndicator.style.zIndex = '10';
|
|
avatarContainer.appendChild(loadingIndicator);
|
|
|
|
tween(1, 0, 300,
|
|
(value) => {
|
|
if (avatarImage) avatarImage.style.opacity = value;
|
|
},
|
|
() => {}
|
|
);
|
|
|
|
const poll = async () => {
|
|
attempts++;
|
|
try {
|
|
const response = await fetch('/avatar/3d');
|
|
if (!response.ok) throw new Error('Network response was not ok');
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.state === 'Completed' && data.imageUrl) {
|
|
const modelResponse = await fetch(`/cdn_local/${data.imageUrl}`);
|
|
if (!modelResponse.ok) throw new Error('Failed to fetch model config');
|
|
|
|
const modelData = await modelResponse.json();
|
|
current3DHash = data.imageUrl;
|
|
|
|
const loadingSpinner = document.getElementById('3d-avatar-spinner');
|
|
if (loadingSpinner) loadingSpinner.remove();
|
|
|
|
render3DAvatar(modelData);
|
|
} else if (data.state === 'Pending' && attempts < maxAttempts) {
|
|
setTimeout(poll, 500);
|
|
} else {
|
|
throw new Error('Avatar rendering failed or timed out');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in 3D avatar polling:', error);
|
|
|
|
const loadingSpinner = document.getElementById('3d-avatar-spinner');
|
|
if (loadingSpinner) loadingSpinner.remove();
|
|
|
|
show2DAvatar();
|
|
|
|
if (attempts >= maxAttempts) {
|
|
const currentAvatarContainer = document.querySelector('.avatar-image-container');
|
|
if (currentAvatarContainer) {
|
|
var errorIcon = document.createElement('div');
|
|
errorIcon.className = 'error-indicator';
|
|
errorIcon.innerHTML = '<span class="icon-spot-error-2xl"></span><div>Failed to load 3D avatar</div>';
|
|
errorIcon.style.position = 'absolute';
|
|
errorIcon.style.top = '50%';
|
|
errorIcon.style.left = '50%';
|
|
errorIcon.style.transform = 'translate(-50%, -50%)';
|
|
errorIcon.style.textAlign = 'center';
|
|
errorIcon.style.color = '#ff3333';
|
|
currentAvatarContainer.appendChild(errorIcon);
|
|
|
|
setTimeout(() => {
|
|
if (errorIcon && errorIcon.parentNode) {
|
|
errorIcon.remove();
|
|
}
|
|
}, 3000);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
await poll();
|
|
}
|
|
|
|
async function request3DAvatar() {
|
|
const avatarContainer = document.querySelector('.avatar-image-container');
|
|
if (!avatarContainer) {
|
|
console.error('Avatar container not found');
|
|
return;
|
|
}
|
|
|
|
const loadingIndicator = document.createElement('div');
|
|
loadingIndicator.id = 'request-3d-loading';
|
|
loadingIndicator.className = 'loading-indicator';
|
|
loadingIndicator.innerHTML = `
|
|
<div class="spinner-container">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
<div class="loading-text">Requesting 3D Avatar...</div>
|
|
`;
|
|
loadingIndicator.style.position = 'absolute';
|
|
loadingIndicator.style.top = '50%';
|
|
loadingIndicator.style.left = '50%';
|
|
loadingIndicator.style.transform = 'translate(-50%, -50%)';
|
|
loadingIndicator.style.textAlign = 'center';
|
|
loadingIndicator.style.zIndex = '10';
|
|
avatarContainer.appendChild(loadingIndicator);
|
|
|
|
try {
|
|
setTimeout(() => {
|
|
const initialLoading = document.getElementById('request-3d-loading');
|
|
if (initialLoading) initialLoading.remove();
|
|
pollFor3DAvatar();
|
|
}, 500);
|
|
} catch (error) {
|
|
console.error('Error requesting 3D avatar:', error);
|
|
const loadingIndicator = document.getElementById('request-3d-loading');
|
|
if (loadingIndicator) loadingIndicator.remove();
|
|
show2DAvatar();
|
|
}
|
|
}
|
|
function render3DAvatar(modelData) {
|
|
const avatarContainer = document.querySelector('.avatar-image-container');
|
|
|
|
if (!avatarContainer) {
|
|
console.error('Avatar container not found for 3D rendering');
|
|
return;
|
|
}
|
|
|
|
const existingCanvas = avatarContainer.querySelector('canvas');
|
|
if (existingCanvas) existingCanvas.remove();
|
|
|
|
const canvas = document.createElement('canvas');
|
|
canvas.style.width = '100%';
|
|
canvas.style.height = '100%';
|
|
canvas.style.position = 'absolute';
|
|
canvas.style.zIndex = '1';
|
|
avatarContainer.appendChild(canvas);
|
|
|
|
const scene = new THREE.Scene();
|
|
scene.background = null;
|
|
|
|
const camera = new THREE.PerspectiveCamera(
|
|
45,
|
|
canvas.clientWidth / canvas.clientHeight,
|
|
0.1,
|
|
1000
|
|
);
|
|
|
|
const avatarGroup = new THREE.Group();
|
|
scene.add(avatarGroup);
|
|
|
|
avatarGroup.position.set(0, 0, 0);
|
|
|
|
addLightsToScene(scene, camera, false);
|
|
|
|
const renderer = new THREE.WebGLRenderer({
|
|
canvas,
|
|
antialias: true,
|
|
alpha: true,
|
|
preserveDrawingBuffer: true
|
|
});
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
|
|
|
|
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.screenSpacePanning = false;
|
|
controls.minDistance = 0;
|
|
controls.maxDistance = 15;
|
|
controls.maxPolarAngle = Math.PI / 1.5;
|
|
controls.autoRotate = true;
|
|
controls.autoRotateSpeed = 3.0;
|
|
|
|
const mtlLoader = new THREE.MTLLoader();
|
|
const objLoader = new THREE.OBJLoader();
|
|
const textureLoader = new THREE.TextureLoader();
|
|
|
|
const modelLoadingIndicator = document.createElement('div');
|
|
modelLoadingIndicator.id = 'model-loading-indicator';
|
|
modelLoadingIndicator.className = 'loading-indicator';
|
|
modelLoadingIndicator.innerHTML = `
|
|
<div class="spinner-container">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
<div class="loading-text">Loading Model...</div>
|
|
`;
|
|
modelLoadingIndicator.style.position = 'absolute';
|
|
modelLoadingIndicator.style.top = '50%';
|
|
modelLoadingIndicator.style.left = '50%';
|
|
modelLoadingIndicator.style.transform = 'translate(-50%, -50%)';
|
|
modelLoadingIndicator.style.textAlign = 'center';
|
|
modelLoadingIndicator.style.zIndex = '10';
|
|
avatarContainer.appendChild(modelLoadingIndicator);
|
|
|
|
fetch(`/cdn_local/${modelData.mtl}`)
|
|
.then(response => response.text())
|
|
.then(mtlContent => {
|
|
const cleanMTLContent = mtlContent
|
|
.replace(/^map_Ka\s+.*$/gm, '')
|
|
.replace(/^map_Kd\s+.*$/gm, '')
|
|
.replace(/^map_d\s+.*$/gm, '')
|
|
.replace(/^\s*[\r\n]/gm, '');
|
|
|
|
const materials = mtlLoader.parse(cleanMTLContent);
|
|
materials.preload();
|
|
|
|
for (const key in materials.materials) {
|
|
if (materials.materials.hasOwnProperty(key)) {
|
|
const material = materials.materials[key];
|
|
material.transparent = false;
|
|
material.opacity = 1.0;
|
|
material.side = THREE.DoubleSide;
|
|
material.alphaTest = 0;
|
|
material.depthWrite = true;
|
|
material.depthTest = true;
|
|
|
|
material.map = null;
|
|
material.mapKa = null;
|
|
material.mapKd = null;
|
|
material.mapKs = null;
|
|
material.mapD = null;
|
|
|
|
material.color = material.color || new THREE.Color(0xffffff);
|
|
material.specular = material.specular || new THREE.Color(0x111111);
|
|
material.shininess = material.shininess || 30;
|
|
}
|
|
}
|
|
|
|
objLoader.setMaterials(materials);
|
|
|
|
objLoader.load(`/cdn_local/${modelData.obj}`, (object) => {
|
|
const loadingIndicator = document.getElementById('model-loading-indicator');
|
|
if (loadingIndicator) loadingIndicator.remove();
|
|
|
|
avatarGroup.add(object);
|
|
|
|
const bbox = new THREE.Box3().setFromObject(object);
|
|
const center = new THREE.Vector3();
|
|
bbox.getCenter(center);
|
|
object.position.sub(center);
|
|
|
|
const size = bbox.getSize(new THREE.Vector3());
|
|
const maxDim = Math.max(size.x, size.y, size.z);
|
|
const scale = 2 / maxDim;
|
|
object.scale.set(scale, scale, scale);
|
|
|
|
const newBbox = new THREE.Box3().setFromObject(object);
|
|
const newCenter = new THREE.Vector3();
|
|
newBbox.getCenter(newCenter);
|
|
object.position.sub(newCenter);
|
|
|
|
camera.position.z = -3;
|
|
camera.lookAt(avatarGroup.position);
|
|
|
|
if (modelData.textures && modelData.textures.length > 0) {
|
|
textureLoader.load(`/cdn_local/${modelData.textures[0]}`, (texture) => {
|
|
object.traverse((child) => {
|
|
if (child.isMesh) {
|
|
child.material.map = texture;
|
|
child.material.needsUpdate = true;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
animate();
|
|
|
|
function handleResize() {
|
|
const width = canvas.clientWidth;
|
|
const height = canvas.clientHeight;
|
|
camera.aspect = width / height;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(width, height, false);
|
|
}
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
setTimeout(handleResize, 0);
|
|
|
|
canvas.addEventListener('dblclick', () => controls.reset());
|
|
controls.addEventListener('start', () => controls.autoRotate = false);
|
|
controls.addEventListener('end', () => {
|
|
setTimeout(() => controls.autoRotate = true, 1000);
|
|
});
|
|
|
|
},
|
|
(xhr) => {
|
|
const loadingText = document.querySelector('#model-loading-indicator .loading-text');
|
|
if (loadingText) {
|
|
const percentComplete = Math.round((xhr.loaded / xhr.total) * 100);
|
|
loadingText.textContent = `Loading Model... ${percentComplete}%`;
|
|
}
|
|
},
|
|
(error) => {
|
|
console.error('Error loading OBJ model:', error);
|
|
const loadingIndicator = document.getElementById('model-loading-indicator');
|
|
if (loadingIndicator) loadingIndicator.remove();
|
|
show2DAvatar();
|
|
|
|
const currentAvatarContainer = document.querySelector('.avatar-image-container');
|
|
if (currentAvatarContainer) {
|
|
var errorIcon = document.createElement('div');
|
|
errorIcon.className = 'error-indicator';
|
|
errorIcon.innerHTML = '<span class="icon-spot-error-2xl"></span><div>Failed to load 3D model</div>';
|
|
errorIcon.style.position = 'absolute';
|
|
errorIcon.style.top = '50%';
|
|
errorIcon.style.left = '50%';
|
|
errorIcon.style.transform = 'translate(-50%, -50%)';
|
|
errorIcon.style.textAlign = 'center';
|
|
errorIcon.style.color = '#ff3333';
|
|
currentAvatarContainer.appendChild(errorIcon);
|
|
|
|
setTimeout(() => {
|
|
if (errorIcon && errorIcon.parentNode) {
|
|
errorIcon.remove();
|
|
}
|
|
}, 3000);
|
|
}
|
|
});
|
|
|
|
}, (error) => {
|
|
console.error('Error loading MTL file:', error);
|
|
const loadingIndicator = document.getElementById('model-loading-indicator');
|
|
if (loadingIndicator) loadingIndicator.remove();
|
|
show2DAvatar();
|
|
|
|
const currentAvatarContainer = document.querySelector('.avatar-image-container');
|
|
if (currentAvatarContainer) {
|
|
var errorIcon = document.createElement('div');
|
|
errorIcon.className = 'error-indicator';
|
|
errorIcon.innerHTML = '<span class="icon-spot-error-2xl"></span><div>Failed to load material</div>';
|
|
errorIcon.style.position = 'absolute';
|
|
errorIcon.style.top = '50%';
|
|
errorIcon.style.left = '50%';
|
|
errorIcon.style.transform = 'translate(-50%, -50%)';
|
|
errorIcon.style.textAlign = 'center';
|
|
errorIcon.style.color = '#ff3333';
|
|
currentAvatarContainer.appendChild(errorIcon);
|
|
|
|
setTimeout(() => {
|
|
if (errorIcon && errorIcon.parentNode) {
|
|
errorIcon.remove();
|
|
}
|
|
}, 3000);
|
|
}
|
|
});
|
|
}
|
|
|
|
function addLightsToScene(scene, camera, useDynamicLighting) {
|
|
if (useDynamicLighting) {
|
|
const ambient = new THREE.AmbientLight(0x444444);
|
|
camera.add(ambient);
|
|
|
|
const keylight = new THREE.DirectionalLight(0xd4d4d4);
|
|
keylight.target = camera;
|
|
keylight.position.set(-7.5, 0.5, -6.0).normalize();
|
|
camera.add(keylight);
|
|
|
|
const fillLight = new THREE.DirectionalLight(0xacacac);
|
|
fillLight.target = camera;
|
|
fillLight.position.set(20.0, 4.0, -0).normalize();
|
|
camera.add(fillLight);
|
|
|
|
const rimLight = new THREE.DirectionalLight(0xacacac);
|
|
rimLight.target = camera;
|
|
rimLight.position.set(0, 1, 1).normalize();
|
|
camera.add(rimLight);
|
|
} else {
|
|
const ambient = new THREE.AmbientLight(0x878780);
|
|
scene.add(ambient);
|
|
|
|
const sunLight = new THREE.DirectionalLight(0xacacac);
|
|
sunLight.position.set(-0.671597898, 0.671597898, 0.312909544).normalize();
|
|
scene.add(sunLight);
|
|
|
|
const backLight = new THREE.DirectionalLight(0x444444);
|
|
const backLightPos = new THREE.Vector3()
|
|
.copy(sunLight.position)
|
|
.negate()
|
|
.normalize();
|
|
backLight.position.set(backLightPos);
|
|
scene.add(backLight);
|
|
}
|
|
}
|
|
function show2DAvatar() {
|
|
const avatarImage = document.getElementById('avatar-image');
|
|
const avatarContainer = document.querySelector('.avatar-image-container');
|
|
|
|
if (!avatarContainer) {
|
|
console.error('Avatar container not found');
|
|
return;
|
|
}
|
|
|
|
const existingCanvas = avatarContainer.querySelector('canvas');
|
|
if (existingCanvas) {
|
|
existingCanvas.remove();
|
|
}
|
|
|
|
if (avatarImage) {
|
|
avatarImage.style.opacity = 1;
|
|
}
|
|
}
|
|
|
|
|
|
async function RedrawAvatar() {
|
|
if (is3DViewEnabled) {
|
|
await request3DAvatar();
|
|
} else {
|
|
var RedrawResponse = await fetch('/avatar/forceredraw', {
|
|
method: 'POST'
|
|
});
|
|
if (RedrawResponse.status == 200) {
|
|
WaitForRenderReady();
|
|
} else if (RedrawResponse.status == 400) {
|
|
alert("Something went wrong!");
|
|
} else if (RedrawResponse.status == 429) {
|
|
alert("Slow down! You recently asked for a redraw!");
|
|
} else {
|
|
alert("Something went wrong!");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
async function WaitForRenderReady() {
|
|
var RenderWaitingText = document.getElementById('render-waiting-text')
|
|
RenderWaitingText.style.display = 'block'
|
|
while (true) {
|
|
var RenderReadyResponse = await fetch('/avatar/isthumbnailready')
|
|
if (RenderReadyResponse.status == 200) {
|
|
var RenderReadyResponseJSON = await RenderReadyResponse.json()
|
|
if (RenderReadyResponseJSON["ready"]) {
|
|
break
|
|
}
|
|
}
|
|
await new Promise(r => setTimeout(r, 500));
|
|
}
|
|
RenderWaitingText.style.display = 'none'
|
|
var AvatarImage = document.getElementById('avatar-image')
|
|
var AvatarSource = AvatarImage.src // Unmodified src "/Thumbs/Avatar.ashx?x=420&y=420&userId=1"
|
|
if (AvatarSource.includes('&refresh=')) {
|
|
AvatarSource = AvatarSource.substring(0, AvatarSource.indexOf('&refresh='))
|
|
}
|
|
AvatarImage.src = AvatarSource + '&refresh=' + Math.random()
|
|
}
|
|
|
|
async function UpdateAvatar() {
|
|
var SaveChangesButton = document.getElementById('savechanges-btn')
|
|
SaveChangesButton.style.display = 'none'
|
|
var CurrentlyEquippedArray = []
|
|
for (let i = 0; i < AssetsCurrentlyEquipped.length; i++) {
|
|
CurrentlyEquippedArray.push(AssetTypeDict[AssetsCurrentlyEquipped[i]].id)
|
|
}
|
|
var UpdateAvatarResponse = await fetch('/avatar/setavatar', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
'bodyColors': [UserBodyColors.Head, UserBodyColors.Torso, UserBodyColors.LeftArm, UserBodyColors.RightArm, UserBodyColors.LeftLeg, UserBodyColors.RightLeg],
|
|
"assets": CurrentlyEquippedArray,
|
|
"rigType": rigType,
|
|
"scales": UserAvatarScaling
|
|
})
|
|
})
|
|
if (UpdateAvatarResponse.status == 200) {
|
|
WaitForRenderReady()
|
|
} else if (UpdateAvatarResponse.status == 400) {
|
|
alert('Something went wrong!')
|
|
} else if (UpdateAvatarResponse.status == 429) {
|
|
alert('Slow down! You are changing your avatar too fast!')
|
|
SaveChangesButton.style.display = 'block'
|
|
} else {
|
|
alert('Something went wrong!')
|
|
}
|
|
}
|
|
|
|
async function ConvertBodyColortoHex( bodycolor ) {
|
|
for (let i = 0; i < AvatarRules.bodyColorsPalette.length; i++) {
|
|
if (AvatarRules.bodyColorsPalette[i].brickColorId == bodycolor) {
|
|
return AvatarRules.bodyColorsPalette[i].hexColor
|
|
}
|
|
}
|
|
return '#FFFFFF'
|
|
}
|
|
|
|
async function UpdateAssetCards( assetId ) {
|
|
var AssetCards = document.getElementsByClassName(`asset-card-${assetId}`)
|
|
var IsAssetEquipped = AssetsCurrentlyEquipped.includes(assetId)
|
|
for (let i = 0; i < AssetCards.length; i++) {
|
|
var ToggleWearButton = AssetCards[i].getElementsByClassName('asset-card-button')[0]
|
|
if (IsAssetEquipped) {
|
|
ToggleWearButton.innerHTML = 'Remove'
|
|
} else {
|
|
ToggleWearButton.innerHTML = 'Wear'
|
|
if (AssetCards[i].getAttribute("data-isundercurrentwearing") == "true") {
|
|
AssetCards[i].remove()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function WearAsset( assetId ) {
|
|
if (AssetsCurrentlyEquipped.includes(assetId)) {
|
|
return
|
|
}
|
|
assetType = AssetTypeDict[assetId].assetType
|
|
var LimitForAssetType = await GetLimitForAssetType(assetType)
|
|
var CountForAssetType = 0
|
|
for (let i = 0; i < AssetsCurrentlyEquipped.length; i++) {
|
|
if (AssetTypeDict[AssetsCurrentlyEquipped[i]].assetType == assetType) {
|
|
CountForAssetType++
|
|
}
|
|
}
|
|
if (CountForAssetType >= LimitForAssetType) {
|
|
for (let i = 0; i < AssetsCurrentlyEquipped.length; i++) {
|
|
if (AssetTypeDict[AssetsCurrentlyEquipped[i]].assetType == assetType) {
|
|
var AssetId = AssetsCurrentlyEquipped[i]
|
|
AssetsCurrentlyEquipped.splice(i, 1)
|
|
UpdateAssetCards(AssetId)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if (LimitForAssetType > 0) {
|
|
AssetsCurrentlyEquipped.push(assetId)
|
|
var CurrentlyWearingCardHolder = document.getElementById('currentlywearing-card-holder')
|
|
var NewAssetCard = await CreateNewAssetCard(assetId, AssetTypeDict[assetId].name)
|
|
CurrentlyWearingCardHolder.appendChild(NewAssetCard)
|
|
NewAssetCard.setAttribute("data-isundercurrentwearing", "true")
|
|
}
|
|
UpdateAssetCards(assetId)
|
|
document.getElementById('savechanges-btn').style.display = 'block'
|
|
}
|
|
|
|
async function RemoveAsset( assetId ) {
|
|
for (let i = 0; i < AssetsCurrentlyEquipped.length; i++) {
|
|
if (AssetsCurrentlyEquipped[i] == assetId) {
|
|
AssetsCurrentlyEquipped.splice(i, 1)
|
|
break
|
|
}
|
|
}
|
|
UpdateAssetCards(assetId)
|
|
document.getElementById('savechanges-btn').style.display = 'block'
|
|
}
|
|
|
|
async function CreateNewAssetCard( AssetId, Name ) {
|
|
var NewAssetCard = AssetCardTemplate.cloneNode(true)
|
|
NewAssetCard.setAttribute("class", NewAssetCard.getAttribute("class").replace("template", AssetId))
|
|
NewAssetCard.style.display = 'block'
|
|
var AssetImage = NewAssetCard.getElementsByClassName('asset-card-img')[0]
|
|
var AssetName = NewAssetCard.getElementsByClassName('asset-card-assetname')[0]
|
|
var AssetToggleButton = NewAssetCard.getElementsByClassName('asset-card-button')[0]
|
|
AssetImage.src = `/Thumbs/Asset.ashx?x=150&y=150&assetId=${AssetId}`
|
|
AssetName.innerText = Name
|
|
AssetName.href = `/catalog/${AssetId}/`
|
|
|
|
//var AssetsCardHolder = document.getElementById('assets-card-holder')
|
|
//AssetsCardHolder.appendChild(NewAssetCard)
|
|
|
|
if (AssetsCurrentlyEquipped.includes(AssetId)) {
|
|
AssetToggleButton.innerText = 'Remove'
|
|
}
|
|
if ( AssetTypeDict[AssetId].moderation_status == 0 ) {
|
|
AssetToggleButton.addEventListener('click', async () => {
|
|
if (AssetToggleButton.innerText == 'Wear') {
|
|
AssetToggleButton.innerText = 'Remove'
|
|
WearAsset(AssetId)
|
|
} else if (AssetToggleButton.innerText == 'Remove') {
|
|
AssetToggleButton.innerText = 'Wear'
|
|
RemoveAsset(AssetId)
|
|
}
|
|
})
|
|
} else {
|
|
AssetToggleButton.disabled = true
|
|
if ( AssetTypeDict[AssetId].moderation_status == 1 ) {
|
|
AssetToggleButton.innerText = 'Pending'
|
|
} else if ( AssetTypeDict[AssetId].moderation_status == 2 ) {
|
|
AssetToggleButton.innerText = 'Deleted'
|
|
}
|
|
}
|
|
|
|
return NewAssetCard
|
|
}
|
|
|
|
async function LoadPageForAsset( AssetType ) {
|
|
var AssetsArray = await GetAssetsForType(AssetType, PageNumber)
|
|
var AssetsCardHolder = document.getElementById('assets-card-holder')
|
|
AssetsCardHolder.innerHTML = ''
|
|
for (let i = 0; i < AssetsArray.length; i++) {
|
|
var Asset = AssetsArray[i]
|
|
var AssetCard = await CreateNewAssetCard(Asset.id, Asset.name)
|
|
AssetsCardHolder.appendChild(AssetCard)
|
|
}
|
|
|
|
if (PageNumber > 0) {
|
|
PaginationPreviousBtn.disabled = false
|
|
} else {
|
|
PaginationPreviousBtn.disabled = true
|
|
}
|
|
if (!IsThisTheLastPage) {
|
|
PaginationNextBtn.disabled = false
|
|
} else {
|
|
PaginationNextBtn.disabled = true
|
|
}
|
|
if (AssetsArray.length == 0) {
|
|
AssetsCardHolder.innerHTML = '<p style="text-align: center;" class="mt-3">No items found</p>'
|
|
}
|
|
}
|
|
|
|
var ColorPickerSelectedBodyPart = -1
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
const ResponseAvatarRules = await fetch('/static/avatarrules.json?version=2')
|
|
AvatarRules = await ResponseAvatarRules.json()
|
|
AssetCardTemplate = document.getElementsByClassName('asset-card-template')[0]
|
|
PaginationNextBtn = document.getElementById('pagination-next-btn')
|
|
PaginationPreviousBtn = document.getElementById('pagination-back-btn')
|
|
PaginationText = document.getElementById('pagination-page-number')
|
|
const ResponseUserAvatar = await fetch('/avatar/getavatar')
|
|
const UserAvatar = await ResponseUserAvatar.json() // This should respond with {"bodyColors": [headColor, torsoColor, leftArmColor, rightArmColor, leftLegColor, rightLegColor], "currentlyWearing": [assetId]}
|
|
var CurrentlyWearingCardHolder = document.getElementById('currentlywearing-card-holder')
|
|
for (let i = 0; i < UserAvatar.currentlyWearing.length; i++) {
|
|
var Asset = UserAvatar.currentlyWearing[i]
|
|
AssetTypeDict[Asset.id] = {"id": Asset.id, "assetType": Asset.type, "name": Asset.name, "moderation_status": Asset.moderation_status}
|
|
AssetsCurrentlyEquipped.push(Asset.id)
|
|
var AssetCard = await CreateNewAssetCard(Asset.id, Asset.name)
|
|
CurrentlyWearingCardHolder.appendChild(AssetCard)
|
|
AssetCard.setAttribute("data-isundercurrentwearing", "true")
|
|
}
|
|
|
|
UserBodyColors.Head = UserAvatar.bodyColors[0]
|
|
UserBodyColors.Torso = UserAvatar.bodyColors[1]
|
|
UserBodyColors.LeftArm = UserAvatar.bodyColors[2]
|
|
UserBodyColors.RightArm = UserAvatar.bodyColors[3]
|
|
UserBodyColors.LeftLeg = UserAvatar.bodyColors[4]
|
|
UserBodyColors.RightLeg = UserAvatar.bodyColors[5]
|
|
|
|
const SaveChangesButton = document.getElementById('savechanges-btn')
|
|
|
|
|
|
document.getElementById('toggle-3d-btn').addEventListener('click', toggle3DView);
|
|
|
|
if (is3DViewEnabled) {
|
|
document.getElementById('toggle-3d-btn').innerText = '2D';
|
|
checkAndRender3DAvatar();
|
|
} else {
|
|
document.getElementById('toggle-3d-btn').innerText = '3D';
|
|
}
|
|
|
|
|
|
const SelectTabInput = document.getElementById('select-tab-input')
|
|
const EquipAssetsTab = document.getElementById('equip-assets-tab')
|
|
const AvatarScalingTab = document.getElementById('avatar-scaling-tab')
|
|
|
|
SelectTabInput.addEventListener('change', () => {
|
|
var selectedTabIndex = Number(SelectTabInput.options[SelectTabInput.selectedIndex].getAttribute('data-tab'))
|
|
if (selectedTabIndex == 1) {
|
|
EquipAssetsTab.style.display = 'block'
|
|
AvatarScalingTab.style.display = 'none'
|
|
} else if (selectedTabIndex == 2) {
|
|
EquipAssetsTab.style.display = 'none'
|
|
AvatarScalingTab.style.display = 'block'
|
|
}
|
|
})
|
|
// haha i touched it! Fuck ou!!!!
|
|
const HeadColor = document.getElementById('head-bodycolor')
|
|
const TorsoColor = document.getElementById('torso-bodycolor')
|
|
const LeftArmColor = document.getElementById('leftarm-bodycolor')
|
|
const RightArmColor = document.getElementById('rightarm-bodycolor')
|
|
const LeftLegColor = document.getElementById('leftleg-bodycolor')
|
|
const RightLegColor = document.getElementById('rightleg-bodycolor')
|
|
|
|
HeadColor.style.backgroundColor = await ConvertBodyColortoHex(UserBodyColors.Head)
|
|
TorsoColor.style.backgroundColor = await ConvertBodyColortoHex(UserBodyColors.Torso)
|
|
LeftArmColor.style.backgroundColor = await ConvertBodyColortoHex(UserBodyColors.LeftArm)
|
|
RightArmColor.style.backgroundColor = await ConvertBodyColortoHex(UserBodyColors.RightArm)
|
|
LeftLegColor.style.backgroundColor = await ConvertBodyColortoHex(UserBodyColors.LeftLeg)
|
|
RightLegColor.style.backgroundColor = await ConvertBodyColortoHex(UserBodyColors.RightLeg)
|
|
|
|
HeadColor.addEventListener('click', () => {
|
|
ColorPickerSelectedBodyPart = 0
|
|
ColorPickerOverlay.style.display = 'block'
|
|
})
|
|
TorsoColor.addEventListener('click', () => {
|
|
ColorPickerSelectedBodyPart = 1
|
|
ColorPickerOverlay.style.display = 'block'
|
|
})
|
|
LeftArmColor.addEventListener('click', async () => {
|
|
await new Promise(r => setTimeout(r, 100)); // This fixes an issue where the torso and left arm would be selected at the same time
|
|
ColorPickerSelectedBodyPart = 2
|
|
ColorPickerOverlay.style.display = 'block'
|
|
})
|
|
RightArmColor.addEventListener('click', async () => {
|
|
await new Promise(r => setTimeout(r, 100));
|
|
ColorPickerSelectedBodyPart = 3
|
|
ColorPickerOverlay.style.display = 'block'
|
|
})
|
|
LeftLegColor.addEventListener('click', () => {
|
|
ColorPickerSelectedBodyPart = 4
|
|
ColorPickerOverlay.style.display = 'block'
|
|
})
|
|
RightLegColor.addEventListener('click', () => {
|
|
ColorPickerSelectedBodyPart = 5
|
|
ColorPickerOverlay.style.display = 'block'
|
|
})
|
|
|
|
const ColorPickerOverlay = document.getElementById('color-picker-overlay')
|
|
const ColorPickerContent = document.getElementById('color-picker-content')
|
|
const ColorPickerClose = document.getElementById('color-picker-close')
|
|
|
|
ColorPickerClose.addEventListener('click', () => {
|
|
ColorPickerOverlay.style.display = 'none'
|
|
})
|
|
// We need to add each BrickColor to the color picker content
|
|
for (let i = 0; i < AvatarRules.bodyColorsPalette.length; i++) {
|
|
const ColorPickerItem = document.createElement('p')
|
|
ColorPickerItem.classList.add('color-picker-item')
|
|
ColorPickerItem.setAttribute("title", AvatarRules.bodyColorsPalette[i].name)
|
|
ColorPickerItem.setAttribute("data-brickcolorid", AvatarRules.bodyColorsPalette[i].brickColorId)
|
|
ColorPickerItem.style.backgroundColor = AvatarRules.bodyColorsPalette[i].hexColor
|
|
ColorPickerContent.appendChild(ColorPickerItem)
|
|
}
|
|
const ColorPickerItems = document.getElementsByClassName('color-picker-item')
|
|
for (let i = 0; i < ColorPickerItems.length; i++) {
|
|
ColorPickerItems[i].addEventListener('click', async () => {
|
|
if (ColorPickerSelectedBodyPart > -1) {
|
|
var SelectedBrickColor = ColorPickerItems[i].getAttribute('data-brickcolorid')
|
|
if (ColorPickerSelectedBodyPart == 0) {
|
|
HeadColor.style.backgroundColor = await ConvertBodyColortoHex(Number(SelectedBrickColor))
|
|
UserBodyColors.Head = Number(SelectedBrickColor)
|
|
} else if (ColorPickerSelectedBodyPart == 1) {
|
|
TorsoColor.style.backgroundColor = await ConvertBodyColortoHex(Number(SelectedBrickColor))
|
|
UserBodyColors.Torso = Number(SelectedBrickColor)
|
|
} else if (ColorPickerSelectedBodyPart == 2) {
|
|
LeftArmColor.style.backgroundColor = await ConvertBodyColortoHex(Number(SelectedBrickColor))
|
|
UserBodyColors.LeftArm = Number(SelectedBrickColor)
|
|
} else if (ColorPickerSelectedBodyPart == 3) {
|
|
RightArmColor.style.backgroundColor = await ConvertBodyColortoHex(Number(SelectedBrickColor))
|
|
UserBodyColors.RightArm = Number(SelectedBrickColor)
|
|
} else if (ColorPickerSelectedBodyPart == 4) {
|
|
LeftLegColor.style.backgroundColor = await ConvertBodyColortoHex(Number(SelectedBrickColor))
|
|
UserBodyColors.LeftLeg = Number(SelectedBrickColor)
|
|
} else if (ColorPickerSelectedBodyPart == 5) {
|
|
RightLegColor.style.backgroundColor = await ConvertBodyColortoHex(Number(SelectedBrickColor))
|
|
UserBodyColors.RightLeg = Number(SelectedBrickColor)
|
|
}
|
|
}
|
|
SaveChangesButton.style.display = 'block'
|
|
ColorPickerSelectedBodyPart = -1
|
|
ColorPickerOverlay.style.display = 'none'
|
|
})
|
|
}
|
|
|
|
PaginationNextBtn.addEventListener('click', () => {
|
|
if (PaginationNextBtn.disabled){
|
|
return
|
|
}
|
|
PaginationPreviousBtn.disabled = true
|
|
PaginationNextBtn.disabled = true
|
|
if (!IsThisTheLastPage) {
|
|
PageNumber += 1
|
|
LoadPageForAsset(SelectedAssetType)
|
|
}
|
|
PaginationText.innerText = `Page ${PageNumber+1}`
|
|
})
|
|
PaginationPreviousBtn.addEventListener('click', () => {
|
|
if (PaginationPreviousBtn.disabled){
|
|
return
|
|
}
|
|
PaginationPreviousBtn.disabled = true
|
|
PaginationNextBtn.disabled = true
|
|
if (PageNumber > 0) {
|
|
PageNumber -= 1
|
|
LoadPageForAsset(SelectedAssetType)
|
|
}
|
|
PaginationText.innerText = `Page ${PageNumber+1}`
|
|
})
|
|
var SelectAssetTypeElement = document.getElementById('select-asset-type')
|
|
SelectAssetTypeElement.addEventListener('change', () => {
|
|
// The option element has a data-assettype attribute which we can use to get the asset type
|
|
SelectedAssetType = Number(SelectAssetTypeElement.options[SelectAssetTypeElement.selectedIndex].getAttribute('data-type'))
|
|
PageNumber = 0
|
|
PaginationPreviousBtn.disabled = true
|
|
PaginationNextBtn.disabled = true
|
|
PaginationText.innerText = `Page ${PageNumber+1}`
|
|
LoadPageForAsset(SelectedAssetType)
|
|
})
|
|
|
|
const changeRigTypeBtn = document.getElementById('change-rigtype-btn')
|
|
if ( UserAvatar.rigType == "R6" ) {
|
|
changeRigTypeBtn.innerText = "R15"
|
|
} else {
|
|
changeRigTypeBtn.innerText = "R6"
|
|
}
|
|
rigType = UserAvatar.rigType
|
|
changeRigTypeBtn.addEventListener('click', async () => {
|
|
changeRigTypeBtn.innerText = rigType
|
|
if ( rigType == "R6" ) {
|
|
rigType = "R15"
|
|
} else {
|
|
rigType = "R6"
|
|
}
|
|
SaveChangesButton.style.display = 'block'
|
|
})
|
|
|
|
async function HandleScaleSlider( scaleName ) {
|
|
UserAvatarScaling[scaleName] = UserAvatar.scales[scaleName]
|
|
const sliderParent = document.getElementById(`${scaleName}-scale-group`)
|
|
|
|
const SliderInput = sliderParent.getElementsByClassName("scaling-slider")[0]
|
|
const SliderValueText = sliderParent.getElementsByClassName("scaling-value")[0]
|
|
const ScaleRules = AvatarRules.scales[scaleName]
|
|
|
|
SliderInput.min = ScaleRules.min * 100
|
|
SliderInput.max = ScaleRules.max * 100
|
|
SliderInput.step = ScaleRules.step * 100
|
|
SliderInput.value = UserAvatar.scales[scaleName] * 100
|
|
|
|
SliderValueText.innerText = `${UserAvatar.scales[scaleName] * 100}%`
|
|
|
|
SliderInput.addEventListener('input', () => {
|
|
SliderValueText.innerText = `${SliderInput.value}%`
|
|
SaveChangesButton.style.display = 'block'
|
|
UserAvatarScaling[scaleName] = SliderInput.value / 100
|
|
})
|
|
}
|
|
|
|
await HandleScaleSlider( "height" )
|
|
await HandleScaleSlider( "width" )
|
|
await HandleScaleSlider( "head" )
|
|
await HandleScaleSlider( "proportion" )
|
|
|
|
LoadPageForAsset(11) // Default: Shirt
|
|
}) |