tailwind.config = {
corePlugins: { preflight: false },
theme: { extend: { fontFamily: { sans: [‘Inter’, ‘sans-serif’] } } }
}
@import url(‘https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap’);
#aura-root {
font-family: ‘Inter’, sans-serif;
background-color: #111827;
position: relative;
width: 100%;
height: 85vh;
min-height: 600px;
overflow: hidden;
border: 1px solid #374151;
border-radius: 12px;
box-sizing: border-box;
color: white;
}
#aura-root * { box-sizing: border-box; border-width: 0; border-style: solid; border-color: #374151; }
#aura-root input[type=”range”] {
display: block; width: 100%; height: 4px; background: #4b5563; border-radius: 2px; -webkit-appearance: none;
}
#aura-root input[type=”range”]::-webkit-slider-thumb {
-webkit-appearance: none; width: 16px; height: 16px; background: white; border-radius: 50%; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
#aura-root button { cursor: pointer; transition: all 0.15s ease; }
/* Scrollbar */
#aura-root .glass-panel::-webkit-scrollbar { width: 4px; }
#aura-root .glass-panel::-webkit-scrollbar-track { background: transparent; }
#aura-root .glass-panel::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 4px; }
#aura-root .loader {
border: 3px solid rgba(255,255,255,0.1); border-top: 3px solid #38bdf8; border-radius: 50%;
width: 30px; height: 30px; animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
#viewer-container canvas { width: 100%; height: 100%; display: block; outline: none; }
/* Color Picker Styling */
#aura-root .color-picker-wrapper { display: flex; align-items: center; justify-content: center; margin-top: 4px; }
#aura-root .color-picker-wrapper input[type=”color”] {
-webkit-appearance: none; width: 30px; height: 30px; padding: 0; border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; background: transparent; overflow: hidden; cursor: pointer;
}
#aura-root .color-picker-wrapper input[type=”color”]::-webkit-color-swatch-wrapper { padding: 0; }
#aura-root .color-picker-wrapper input[type=”color”]::-webkit-color-swatch { border: none; border-radius: 50%; }
.label-text { font-size: 0.7rem; font-weight: 600; color: #9ca3af; margin-bottom: 0.4rem; display: block; letter-spacing: 0.02em; }
.group-title { font-size: 0.7rem; font-weight: 800; color: #38bdf8; text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 1rem; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 4px; display: flex; align-items: center; gap: 0.5rem;}
.ui-transition { transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s; }
.ui-hidden { transform: translateX(-110%); opacity: 0; }
📷
Aura 360
Load an image to begin editing
Stability.ai API Key
Save Key
{ “imports”: { “three”: “https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.js”, “three/addons/”: “https://cdn.jsdelivr.net/npm/three@0.165.0/examples/jsm/” } }
import * as THREE from ‘three’;
import { OrbitControls } from ‘three/addons/controls/OrbitControls.js’;
// — XMP Metadata Injector (Google Maps Fix) —
function addXmp(base64Img, width, height) {
const piece1 = “http://ns.adobe.com/xap/1.0/\x00″;
const piece2 = `
True
Aura 360
equirectangular
0.0
0.0
0.0
${width}
${height}
0
0
${width}
${height}
`;
const binaryStr = atob(base64Img.split(‘,’)[1]);
let len = binaryStr.length;
const data = new Uint8Array(len);
for (let i = 0; i < len; i++) data[i] = binaryStr.charCodeAt(i);
const xmpBytes = new Uint8Array(piece1.length + piece2.length);
for (let i = 0; i < piece1.length; i++) xmpBytes[i] = piece1.charCodeAt(i);
for (let i = 0; i > 8) & 0xFF; app1[3] = length & 0xFF;
app1.set(xmpBytes, 4);
const result = new Uint8Array(data.length + app1.length);
result.set(data.subarray(0, 2), 0);
result.set(app1, 2);
result.set(data.subarray(2), 2 + app1.length);
return new Blob([result], { type: ‘image/jpeg’ });
}
const vertexShader = `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`;
const fragmentShader = `
varying vec2 vUv;
uniform sampler2D uTexture;
uniform vec2 uTextureSize;
uniform float uBrightness;
uniform float uContrast;
uniform float uSaturation;
uniform float uVibrance;
uniform float uShadows;
uniform float uHighlights;
uniform float uTemperature;
uniform float uTint;
uniform vec3 uHighlightColor;
uniform vec3 uShadowColor;
uniform float uSharpen;
uniform float uDenoise;
uniform float uStructure;
uniform float uSkySmooth;
uniform float uGroundDetail;
uniform float uHorizonFade;
const vec3 luma = vec3(0.299, 0.587, 0.114);
vec3 blur(sampler2D tex, vec2 uv, vec2 texSize, float strength) {
float radius = strength * 1.5;
float dx = radius / texSize.x;
float dy = radius / texSize.y;
// FIXED WEIGHTS (Sum = 1.0)
vec3 col = texture2D(tex, uv).rgb * 0.25; // Center
col += texture2D(tex, uv + vec2(dx, 0.0)).rgb * 0.125;
col += texture2D(tex, uv + vec2(-dx, 0.0)).rgb * 0.125;
col += texture2D(tex, uv + vec2(0.0, dy)).rgb * 0.125;
col += texture2D(tex, uv + vec2(0.0, -dy)).rgb * 0.125;
col += texture2D(tex, uv + vec2(dx, dy)).rgb * 0.0625;
col += texture2D(tex, uv + vec2(-dx, dy)).rgb * 0.0625;
col += texture2D(tex, uv + vec2(dx, -dy)).rgb * 0.0625;
col += texture2D(tex, uv + vec2(-dx, -dy)).rgb * 0.0625;
return col;
}
void main() {
vec3 original = texture2D(uTexture, vUv).rgb;
vec3 color = original;
// Masks
float horizon = 0.5;
float mask = smoothstep(horizon – uHorizonFade, horizon + uHorizonFade, vUv.y);
// Detail
float blurStrength = uDenoise + (uSkySmooth * mask * 3.0);
if (blurStrength > 0.0) {
color = mix(color, blur(uTexture, vUv, uTextureSize, blurStrength), clamp(blurStrength, 0.0, 1.0));
}
vec3 structBlur = blur(uTexture, vUv, uTextureSize, 1.0);
vec3 diff = original – structBlur;
float detailAmp = uSharpen + uStructure + (uGroundDetail * (1.0 – mask));
color += diff * detailAmp;
// Color Grading
color = clamp(color, 0.0, 1.0);
color.r += uTemperature; color.b -= uTemperature; color.g += uTint;
color = clamp(color, 0.0, 1.0);
color = pow(color, vec3(1.0 – uShadows * 0.5));
color = 1.0 – pow(vec3(1.0) – color, vec3(1.0 + uHighlights * 2.0));
color = clamp(color, 0.0, 1.0);
color = (color – 0.5) * uContrast + 0.5;
color += uBrightness;
color = clamp(color, 0.0, 1.0);
// VIBRANCE & SATURATION
float l = dot(color, luma);
float maxComp = max(color.r, max(color.g, color.b));
float minComp = min(color.r, min(color.g, color.b));
float sat = maxComp – minComp;
float vibFactor = 1.0 + (uVibrance * (1.0 – sat));
color = mix(vec3(l), color, vibFactor);
color = mix(vec3(l), color, uSaturation);
// Split Toning
color = mix(color, color * uShadowColor, (1.0 – l));
color += (uHighlightColor – vec3(1.0)) * l * 0.5;
color += (uShadowColor – vec3(1.0)) * (1.0 – l) * 0.5;
color = clamp(color, 0.0, 1.0);
gl_FragColor = vec4(color, 1.0);
}
`;
let scene, camera, renderer, controls, sphereMesh;
let textureLoader = new THREE.TextureLoader();
let originalTexture = null, originalFile = null;
let customMaterial, stabilityApiKey = null;
let currentAiMode = null;
const viewerContainer = document.getElementById(‘viewer-container’);
const loadingMessage = document.getElementById(‘loading-message’);
const sidebar = document.getElementById(‘glass-sidebar’);
const toggleUiBtn = document.getElementById(‘toggle-ui-btn’);
const iconEye = document.getElementById(‘icon-eye’);
const iconEyeOff = document.getElementById(‘icon-eye-off’);
const fileInput = document.getElementById(‘aura-file-upload’);
const downloadBtn = document.getElementById(‘aura-download-btn’);
const highlightPicker = document.getElementById(‘aura-highlight-color’);
const shadowPicker = document.getElementById(‘aura-shadow-color’);
function init() {
scene = new THREE.Scene();
const width = viewerContainer.clientWidth;
const height = viewerContainer.clientHeight;
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.set(0, 0, 0.1);
scene.add(camera);
renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true, alpha: true });
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio);
viewerContainer.appendChild(renderer.domElement);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = false; controls.enablePan = false; controls.enableDamping = true; controls.dampingFactor = 0.1; controls.rotateSpeed = -0.25;
viewerContainer.addEventListener(‘wheel’, onMouseWheel, { passive: false });
const resizeObserver = new ResizeObserver(() => { onWindowResize(); });
resizeObserver.observe(viewerContainer);
animate();
loadApiKey();
}
function onMouseWheel(event) {
event.preventDefault();
camera.fov = Math.max(20, Math.min(100, camera.fov + event.deltaY * 0.05));
camera.updateProjectionMatrix();
}
function create360Sphere(texture) {
originalTexture = texture;
const textureSize = new THREE.Vector2(texture.image.width, texture.image.height);
if (sphereMesh) {
customMaterial.uniforms.uTexture.value = texture;
customMaterial.uniforms.uTextureSize.value = textureSize;
customMaterial.needsUpdate = true;
} else {
const geometry = new THREE.SphereGeometry(500, 60, 40);
geometry.scale(-1, 1, 1);
customMaterial = new THREE.ShaderMaterial({
uniforms: {
uTexture: { value: texture },
uTextureSize: { value: textureSize },
uBrightness: { value: 0.0 }, uContrast: { value: 1.0 },
uSaturation: { value: 1.0 }, uVibrance: { value: 0.0 },
uShadows: { value: 0.0 }, uHighlights: { value: 0.0 },
uTemperature: { value: 0.0 }, uTint: { value: 0.0 },
uSharpen: { value: 0.0 }, uDenoise: { value: 0.0 }, uStructure: { value: 0.0 },
uSkySmooth: { value: 0.0 }, uGroundDetail: { value: 0.0 }, uHorizonFade: { value: 0.1 },
uHighlightColor: { value: new THREE.Color(1, 1, 1) },
uShadowColor: { value: new THREE.Color(1, 1, 1) }
},
vertexShader: vertexShader,
fragmentShader: fragmentShader
});
sphereMesh = new THREE.Mesh(geometry, customMaterial);
scene.add(sphereMesh);
}
resetSliders();
if (loadingMessage) loadingMessage.style.display = ‘none’;
}
function resetSliders() {
document.querySelectorAll(‘input[type=”range”]’).forEach(el => {
if (el.id.includes(‘contrast’) || el.id.includes(‘saturation’)) el.value = 1;
else if (el.id.includes(‘horizon’)) el.value = 0.1;
else el.value = 0;
});
highlightPicker.value = “#ffffff”;
shadowPicker.value = “#ffffff”;
}
function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); }
function onWindowResize() {
if (!viewerContainer || viewerContainer.clientWidth === 0) return;
camera.aspect = viewerContainer.clientWidth / viewerContainer.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(viewerContainer.clientWidth, viewerContainer.clientHeight);
}
function onFileChange(event) {
const file = event.target.files[0];
if (file) {
originalFile = file;
const reader = new FileReader();
reader.onload = (e) => {
textureLoader.load(e.target.result, (texture) => {
// — FIX: DARKNESS ISSUE —
texture.colorSpace = THREE.NoColorSpace;
texture.minFilter = THREE.LinearFilter;
create360Sphere(texture);
});
};
reader.readAsDataURL(file);
}
}
function updateUniform(name, val) { if (customMaterial) customMaterial.uniforms[name].value = parseFloat(val); }
function updateColorUniform(name, hexVal) {
if (customMaterial) {
const col = new THREE.Color(hexVal);
customMaterial.uniforms[name].value.set(col.r, col.g, col.b);
}
}
function toggleUI() {
const isHidden = sidebar.classList.contains(‘ui-hidden’);
if (isHidden) {
sidebar.classList.remove(‘ui-hidden’);
iconEye.classList.remove(‘hidden’);
iconEyeOff.classList.add(‘hidden’);
} else {
sidebar.classList.add(‘ui-hidden’);
iconEye.classList.add(‘hidden’);
iconEyeOff.classList.remove(‘hidden’);
}
}
function downloadImage() {
if (!originalTexture) return alert(‘Please load an image first.’);
toggleModal(true, { loading: true });
setTimeout(() => {
try {
const offscreenCanvas = document.createElement(‘canvas’);
offscreenCanvas.width = originalTexture.image.width;
offscreenCanvas.height = originalTexture.image.height;
const offscreenRenderer = new THREE.WebGLRenderer({ canvas: offscreenCanvas, antialias: true });
const planeScene = new THREE.Scene();
const planeCamera = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0.1, 10);
const planeGeo = new THREE.PlaneGeometry(1, 1);
const downloadMaterial = customMaterial.clone();
downloadMaterial.uniforms.uTexture.value = originalTexture;
planeScene.add(new THREE.Mesh(planeGeo, downloadMaterial));
planeCamera.position.z = 1;
offscreenRenderer.render(planeScene, planeCamera);
// Generate Data
const dataUrl = offscreenRenderer.domElement.toDataURL(‘image/jpeg’, 0.92);
// Inject Metadata
const blob = addXmp(dataUrl, originalTexture.image.width, originalTexture.image.height);
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement(‘a’);
link.download = ‘aura-360-edit.jpg’;
link.href = blobUrl;
link.click();
URL.revokeObjectURL(blobUrl);
offscreenRenderer.dispose(); planeGeo.dispose(); downloadMaterial.dispose();
toggleModal(false);
} catch (error) { alert(error.message); toggleModal(false); }
}, 100);
}
function toggleModal(show, options = {}) {
const modal = document.getElementById(‘ai-modal’);
if (show) {
modal.classList.remove(‘hidden’);
if (options.loading) {
document.getElementById(‘ai-modal-loading’).classList.remove(‘hidden’);
document.getElementById(‘ai-modal-content’).classList.add(‘hidden’);
document.getElementById(‘ai-modal-apikey’).classList.add(‘hidden’);
}
} else {
modal.classList.add(‘hidden’);
}
}
const STABILITY_API_KEY_NAME = ‘aura360_stability_key’;
function loadApiKey() { try { const key = localStorage.getItem(STABILITY_API_KEY_NAME); if (key) stabilityApiKey = key; } catch (e) {} }
function saveApiKey() {
const key = document.getElementById(‘api-key-input’).value.trim();
if (key && key.startsWith(‘sk-‘)) { localStorage.setItem(STABILITY_API_KEY_NAME, key); stabilityApiKey = key; toggleModal(false); if (currentAiMode === ‘enhance’) onAiEnhanceClick(); }
}
function onAiEnhanceClick() {
currentAiMode = ‘enhance’;
if (!originalFile) return alert(‘Load image first.’);
if (!stabilityApiKey) {
toggleModal(true);
document.getElementById(‘ai-modal-loading’).classList.add(‘hidden’);
document.getElementById(‘ai-modal-apikey’).classList.remove(‘hidden’);
return;
}
callStabilityApi_Upscale();
}
async function callStabilityApi_Upscale() {
toggleModal(true, { loading: true });
const formData = new FormData(); formData.append(‘image’, originalFile);
try {
const res = await fetch(‘https://api.stability.ai/v1/generation/esrgan-v1-x2plus/image-to-image/upscale’, {
method: ‘POST’, headers: { ‘Authorization’: `Bearer ${stabilityApiKey}`, ‘Accept’: ‘application/json’ }, body: formData,
});
if (!res.ok) throw new Error((await res.json()).message);
const result = await res.json();
const b64 = result.artifacts[0].base64;
textureLoader.load(`data:image/png;base64,${b64}`, (tex) => {
tex.colorSpace = THREE.NoColorSpace; // FIX
create360Sphere(tex); originalTexture = tex; toggleModal(false);
const byteChars = atob(b64); const byteNumbers = new Array(byteChars.length);
for (let i = 0; i toggleModal(false));
document.getElementById(‘api-key-save-btn’).addEventListener(‘click’, saveApiKey);
const mapUniform = (id, uni) => document.getElementById(id).addEventListener(‘input’, (e) => updateUniform(uni, e.target.value));
mapUniform(‘aura-brightness’, ‘uBrightness’);
mapUniform(‘aura-contrast’, ‘uContrast’);
mapUniform(‘aura-saturation’, ‘uSaturation’);
mapUniform(‘aura-vibrance’, ‘uVibrance’);
mapUniform(‘aura-shadows’, ‘uShadows’);
mapUniform(‘aura-highlights’, ‘uHighlights’);
mapUniform(‘aura-temperature’, ‘uTemperature’);
mapUniform(‘aura-tint’, ‘uTint’);
mapUniform(‘aura-sharpen’, ‘uSharpen’);
mapUniform(‘aura-denoise’, ‘uDenoise’);
mapUniform(‘aura-structure’, ‘uStructure’);
mapUniform(‘aura-sky-smooth’, ‘uSkySmooth’);
mapUniform(‘aura-ground-detail’, ‘uGroundDetail’);
mapUniform(‘aura-horizon-fade’, ‘uHorizonFade’);
highlightPicker.addEventListener(‘input’, (e) => updateColorUniform(‘uHighlightColor’, e.target.value));
shadowPicker.addEventListener(‘input’, (e) => updateColorUniform(‘uShadowColor’, e.target.value));
init();