Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Create Racer Card - WaveType</title> | |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700&family=Exo+2:wght@300;400;600&display=swap" rel="stylesheet"> | |
| <link href="https://unpkg.com/[email protected]/dist/aos.css" rel="stylesheet"> | |
| <script src="https://unpkg.com/[email protected]/dist/aos.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js", | |
| "three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <style> | |
| body { | |
| font-family: 'Exo 2', sans-serif; | |
| background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); | |
| color: #e2e8f0; | |
| min-height: 100vh; | |
| } | |
| .orbitron { | |
| font-family: 'Orbitron', sans-serif; | |
| } | |
| .glow { | |
| text-shadow: 0 0 10px rgba(125, 255, 255, 0.7); | |
| } | |
| .card-bg { | |
| background: rgba(15, 23, 42, 0.7); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(94, 234, 212, 0.3); | |
| } | |
| .btn-primary { | |
| background: linear-gradient(45deg, #00c6ff, #0072ff); | |
| transition: all 0.3s ease; | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 10px 20px rgba(0, 114, 255, 0.3); | |
| } | |
| .form-input { | |
| background: rgba(30, 41, 59, 0.7); | |
| border: 1px solid rgba(94, 234, 212, 0.3); | |
| } | |
| .form-input:focus { | |
| border-color: #00c6ff; | |
| box-shadow: 0 0 0 3px rgba(0, 198, 255, 0.3); | |
| } | |
| .racer-card { | |
| background: linear-gradient(135deg, rgba(15, 23, 42, 0.9), rgba(30, 41, 59, 0.9)); | |
| border: 2px solid transparent; | |
| border-image: linear-gradient(45deg, #00c6ff, #0072ff) 1; | |
| } | |
| .stat-bar { | |
| height: 8px; | |
| background: rgba(30, 41, 59, 0.7); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| } | |
| .stat-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #00c6ff, #0072ff); | |
| } | |
| #threed-canvas { | |
| width: 100%; | |
| height: 200px; | |
| border-radius: 0.5rem; | |
| background-color: rgba(0,0,0,0.2); | |
| } | |
| #model-placeholder { | |
| position: absolute; | |
| inset: 0; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: #94a3b8; | |
| } | |
| #model-container { | |
| position: relative; | |
| height: 200px; | |
| } | |
| .image-preview { | |
| max-width: 100%; | |
| max-height: 200px; | |
| border-radius: 0.5rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Background Elements --> | |
| <div class="absolute inset-0 z-0"> | |
| <div class="absolute top-1/4 left-1/4 w-64 h-64 bg-blue-500 rounded-full filter blur-3xl opacity-20 animate-pulse"></div> | |
| <div class="absolute bottom-1/3 right-1/4 w-72 h-72 bg-purple-500 rounded-full filter blur-3xl opacity-20 animate-pulse"></div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="relative z-10 min-h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="py-6 px-4 sm:px-8"> | |
| <div class="container mx-auto flex justify-between items-center"> | |
| <div class="flex items-center space-x-2"> | |
| <div class="w-10 h-10 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full"></div> | |
| <h1 class="orbitron text-2xl font-bold glow">WAVETYPE</h1> | |
| </div> | |
| <nav> | |
| <ul class="flex space-x-6"> | |
| <li><a href="index.html" class="hover:text-cyan-300 transition">Home</a></li> | |
| <li><a href="index2.html" class="hover:text-cyan-300 transition">Features</a></li> | |
| <li><a href="#" class="hover:text-cyan-300 transition">Contact</a></li> | |
| </ul> | |
| </nav> | |
| </div> | |
| </header> | |
| <!-- Hero Section --> | |
| <main class="flex-grow py-12 px-4"> | |
| <div class="container mx-auto max-w-6xl"> | |
| <div class="text-center mb-12" data-aos="fade-up" data-aos-duration="1000"> | |
| <h1 class="orbitron text-4xl md:text-5xl font-bold mb-6 glow"> | |
| Create a Racer Card | |
| </h1> | |
| <p class="text-xl max-w-3xl mx-auto text-gray-300"> | |
| Design your unique racer profile for Fantasy Rally. This is an unofficial preview of the customization system. | |
| </p> | |
| </div> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-12"> | |
| <!-- Form Section --> | |
| <div data-aos="fade-right" data-aos-duration="1000"> | |
| <div class="card-bg rounded-2xl p-8 border border-cyan-500/30"> | |
| <h2 class="orbitron text-2xl font-bold mb-6">Racer Details</h2> | |
| <form class="space-y-6"> | |
| <div> | |
| <label for="racer-name" class="block mb-2 text-gray-300">Racer Name</label> | |
| <input type="text" id="racer-name" class="form-input w-full px-4 py-3 rounded-lg focus:outline-none"> | |
| </div> | |
| <div> | |
| <label for="team" class="block mb-2 text-gray-300">Team</label> | |
| <select id="team" class="form-input w-full px-4 py-3 rounded-lg focus:outline-none"> | |
| <option>Select a team</option> | |
| <option>Lightning Speed</option> | |
| <option>Thunder Bolt</option> | |
| <option>Fire Storm</option> | |
| <option>Ice Breaker</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block mb-2 text-gray-300">Racer Stats</label> | |
| <div class="space-y-4"> | |
| <div> | |
| <label for="speed" class="block mb-1">Speed</label> | |
| <input type="range" id="speed" min="0" max="100" value="85" class="w-full"> | |
| <div class="flex justify-between text-sm mt-1"> | |
| <span>0</span> | |
| <span id="speed-value">85</span> | |
| <span>100</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="handling" class="block mb-1">Handling</label> | |
| <input type="range" id="handling" min="0" max="100" value="70" class="w-full"> | |
| <div class="flex justify-between text-sm mt-1"> | |
| <span>0</span> | |
| <span id="handling-value">70</span> | |
| <span>100</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="acceleration" class="block mb-1">Acceleration</label> | |
| <input type="range" id="acceleration" min="0" max="100" value="90" class="w-full"> | |
| <div class="flex justify-between text-sm mt-1"> | |
| <span>0</span> | |
| <span id="acceleration-value">90</span> | |
| <span>100</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="reliability" class="block mb-1">Reliability</label> | |
| <input type="range" id="reliability" min="0" max="100" value="75" class="w-full"> | |
| <div class="flex justify-between text-sm mt-1"> | |
| <span>0</span> | |
| <span id="reliability-value">75</span> | |
| <span>100</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block mb-2 text-gray-300">Special Ability</label> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <label class="card-bg rounded-lg p-4 cursor-pointer border border-cyan-500/30 hover:border-cyan-400"> | |
| <input type="radio" name="ability" class="mr-2"> | |
| <span>Nitro Boost</span> | |
| </label> | |
| <label class="card-bg rounded-lg p-4 cursor-pointer border border-cyan-500/30 hover:border-cyan-400"> | |
| <input type="radio" name="ability" class="mr-2"> | |
| <span>Weather Adaptation</span> | |
| </label> | |
| <label class="card-bg rounded-lg p-4 cursor-pointer border border-cyan-500/30 hover:border-cyan-400"> | |
| <input type="radio" name="ability" class="mr-2"> | |
| <span>Tire Mastery</span> | |
| </label> | |
| <label class="card-bg rounded-lg p-4 cursor-pointer border border-cyan-500/30 hover:border-cyan-400"> | |
| <input type="radio" name="ability" class="mr-2"> | |
| <span>Strategic Mind</span> | |
| </label> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block mb-2 text-gray-300">Racer Image</label> | |
| <div class="card-bg rounded-lg p-4 border border-cyan-500/30"> | |
| <input type="file" id="racer-image" class="form-input w-full px-4 py-3 rounded-lg focus:outline-none" accept="image/*"> | |
| <div id="image-preview-container" class="mt-3"></div> | |
| <div class="mt-4"> | |
| <label class="block mb-2 text-gray-300">Suggested Colors</label> | |
| <div class="flex space-x-2"> | |
| <div class="w-8 h-8 rounded-full border border-gray-500" id="color1" style="background-color: #00c6ff;"></div> | |
| <div class="w-8 h-8 rounded-full border border-gray-500" id="color2" style="background-color: #0072ff;"></div> | |
| <button id="apply-colors-btn" class="ml-2 text-sm text-cyan-400 hover:text-cyan-300">Apply Colors</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block mb-2 text-gray-300">3D Model</label> | |
| <div class="card-bg rounded-lg p-4 border border-cyan-500/30"> | |
| <input type="file" id="model-file" class="form-input w-full px-4 py-3 rounded-lg focus:outline-none" accept=".glb,.gltf"> | |
| <div id="model-container" class="mt-3"> | |
| <canvas id="threed-canvas"></canvas> | |
| <div id="model-placeholder"> | |
| <p>Upload a .glb or .gltf model to view</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="pt-4"> | |
| <button type="button" id="create-card-btn" class="btn-primary w-full py-3 rounded-lg text-white font-semibold"> | |
| Create Racer Card | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Preview Section --> | |
| <div data-aos="fade-left" data-aos-duration="1000"> | |
| <div class="sticky top-24"> | |
| <h2 class="orbitron text-2xl font-bold mb-6 text-center">Preview</h2> | |
| <div class="racer-card rounded-2xl p-8"> | |
| <div class="text-center mb-6"> | |
| <div class="w-32 h-32 mx-auto bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full mb-4 flex items-center justify-center"> | |
| <img id="preview-image" src="" alt="Racer Image" class="w-full h-full rounded-full object-cover hidden"> | |
| <i data-feather="user" class="text-white w-16 h-16" id="preview-icon"></i> | |
| </div> | |
| <h3 class="orbitron text-2xl font-bold glow">Racer Name</h3> | |
| <p class="text-cyan-300">Lightning Speed</p> | |
| </div> | |
| <div class="mb-6"> | |
| <h4 class="orbitron text-lg font-bold mb-3">3D Model Preview</h4> | |
| <div id="preview-model-container" class="h-48 rounded-lg bg-gray-800/50 flex items-center justify-center"> | |
| <p id="preview-model-placeholder">No model uploaded</p> | |
| <canvas id="preview-threed-canvas" class="hidden w-full h-full"></canvas> | |
| </div> | |
| </div> | |
| <div class="mb-6"> | |
| <h4 class="orbitron text-lg font-bold mb-3">Stats</h4> | |
| <div class="space-y-3"> | |
| <div> | |
| <div class="flex justify-between text-sm mb-1"> | |
| <span>Speed</span> | |
| <span id="preview-speed-value">85</span> | |
| </div> | |
| <div class="stat-bar"> | |
| <div class="stat-fill" id="preview-speed-bar" style="width: 85%"></div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="flex justify-between text-sm mb-1"> | |
| <span>Handling</span> | |
| <span id="preview-handling-value">70</span> | |
| </div> | |
| <div class="stat-bar"> | |
| <div class="stat-fill" id="preview-handling-bar" style="width: 70%"></div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="flex justify-between text-sm mb-1"> | |
| <span>Acceleration</span> | |
| <span id="preview-acceleration-value">90</span> | |
| </div> | |
| <div class="stat-bar"> | |
| <div class="stat-fill" id="preview-acceleration-bar" style="width: 90%"></div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="flex justify-between text-sm mb-1"> | |
| <span>Reliability</span> | |
| <span id="preview-reliability-value">75</span> | |
| </div> | |
| <div class="stat-bar"> | |
| <div class="stat-fill" id="preview-reliability-bar" style="width: 75%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <h4 class="orbitron text-lg font-bold mb-3">Special Ability</h4> | |
| <div class="card-bg rounded-lg p-4 text-center"> | |
| <i data-feather="zap" class="text-cyan-400 w-6 h-6 mx-auto mb-2"></i> | |
| <p>Nitro Boost</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-8 text-center text-sm text-gray-400"> | |
| <p>*This is an unofficial preview. Actual features may vary.</p> | |
| </div> | |
| <!-- Export Buttons --> | |
| <div class="mt-6 flex flex-col space-y-3"> | |
| <button id="save-png-btn" class="btn-primary py-2 rounded-lg text-white font-semibold hidden"> | |
| Save as PNG | |
| </button> | |
| <button id="save-webp-btn" class="btn-primary py-2 rounded-lg text-white font-semibold hidden"> | |
| Save as WebP | |
| </button> | |
| <button id="save-jpg-btn" class="btn-primary py-2 rounded-lg text-white font-semibold hidden"> | |
| Save as JPG | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="py-8 text-center text-gray-400"> | |
| <div class="container mx-auto px-4"> | |
| <p>© 2023 WaveType. All rights reserved.</p> | |
| <p class="mt-2 text-sm">*Unofficial content may not represent the final product.</p> | |
| </div> | |
| </footer> | |
| </div> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; | |
| // DOM Elements | |
| const racerImageInput = document.getElementById('racer-image'); | |
| const imagePreviewContainer = document.getElementById('image-preview-container'); | |
| const modelFileInput = document.getElementById('model-file'); | |
| const modelContainer = document.getElementById('model-container'); | |
| const modelPlaceholder = document.getElementById('model-placeholder'); | |
| const threedCanvas = document.getElementById('threed-canvas'); | |
| const previewImage = document.getElementById('preview-image'); | |
| const previewIcon = document.getElementById('preview-icon'); | |
| const previewModelContainer = document.getElementById('preview-model-container'); | |
| const previewModelPlaceholder = document.getElementById('preview-model-placeholder'); | |
| const previewThreedCanvas = document.getElementById('preview-threed-canvas'); | |
| // Stats elements | |
| const speedSlider = document.getElementById('speed'); | |
| const handlingSlider = document.getElementById('handling'); | |
| const accelerationSlider = document.getElementById('acceleration'); | |
| const reliabilitySlider = document.getElementById('reliability'); | |
| const speedValue = document.getElementById('speed-value'); | |
| const handlingValue = document.getElementById('handling-value'); | |
| const accelerationValue = document.getElementById('acceleration-value'); | |
| const reliabilityValue = document.getElementById('reliability-value'); | |
| // Preview stats elements | |
| const previewSpeedValue = document.getElementById('preview-speed-value'); | |
| const previewHandlingValue = document.getElementById('preview-handling-value'); | |
| const previewAccelerationValue = document.getElementById('preview-acceleration-value'); | |
| const previewReliabilityValue = document.getElementById('preview-reliability-value'); | |
| const previewSpeedBar = document.getElementById('preview-speed-bar'); | |
| const previewHandlingBar = document.getElementById('preview-handling-bar'); | |
| const previewAccelerationBar = document.getElementById('preview-acceleration-bar'); | |
| const previewReliabilityBar = document.getElementById('preview-reliability-bar'); | |
| // Color elements | |
| const color1 = document.getElementById('color1'); | |
| const color2 = document.getElementById('color2'); | |
| const applyColorsBtn = document.getElementById('apply-colors-btn'); | |
| // Three.js variables for main viewer | |
| let scene, camera, renderer, controls, model; | |
| let previewScene, previewCamera, previewRenderer, previewModel; | |
| // Initialize AOS and Feather icons | |
| AOS.init({ | |
| once: true | |
| }); | |
| feather.replace(); | |
| // Initialize 3D viewers | |
| initMainViewer(); | |
| initPreviewViewer(); | |
| // Initialize main 3D viewer | |
| function initMainViewer() { | |
| // Create scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x0f172a); | |
| // Create camera | |
| camera = new THREE.PerspectiveCamera(75, threedCanvas.clientWidth / threedCanvas.clientHeight, 0.1, 1000); | |
| camera.position.z = 5; | |
| // Create renderer | |
| renderer = new THREE.WebGLRenderer({ | |
| canvas: threedCanvas, | |
| antialias: true, | |
| alpha: true | |
| }); | |
| renderer.setSize(threedCanvas.clientWidth, threedCanvas.clientHeight); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| // Add lights | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(1, 1, 1); | |
| scene.add(directionalLight); | |
| // Add orbit controls | |
| controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.05; | |
| // Start animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| } | |
| // Initialize preview 3D viewer | |
| function initPreviewViewer() { | |
| // Create scene | |
| previewScene = new THREE.Scene(); | |
| previewScene.background = new THREE.Color(0x0f172a); | |
| // Create camera | |
| previewCamera = new THREE.PerspectiveCamera(75, previewThreedCanvas.clientWidth / previewThreedCanvas.clientHeight, 0.1, 1000); | |
| previewCamera.position.z = 5; | |
| // Create renderer | |
| previewRenderer = new THREE.WebGLRenderer({ | |
| canvas: previewThreedCanvas, | |
| antialias: true, | |
| alpha: true | |
| }); | |
| previewRenderer.setSize(previewThreedCanvas.clientWidth, previewThreedCanvas.clientHeight); | |
| previewRenderer.setPixelRatio(window.devicePixelRatio); | |
| // Add lights | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
| previewScene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(1, 1, 1); | |
| previewScene.add(directionalLight); | |
| // Start animation loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| previewRenderer.render(previewScene, previewCamera); | |
| } | |
| animate(); | |
| } | |
| // Handle image upload | |
| racerImageInput.addEventListener('change', function(e) { | |
| if (e.target.files && e.target.files[0]) { | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| // Create image preview | |
| const img = document.createElement('img'); | |
| img.src = event.target.result; | |
| img.className = 'image-preview'; | |
| // Clear previous preview and add new one | |
| imagePreviewContainer.innerHTML = ''; | |
| imagePreviewContainer.appendChild(img); | |
| // Update preview card | |
| previewImage.src = event.target.result; | |
| previewImage.classList.remove('hidden'); | |
| previewIcon.classList.add('hidden'); | |
| // Extract colors from image | |
| extractColors(event.target.result); | |
| } | |
| reader.readAsDataURL(e.target.files[0]); | |
| } | |
| }); | |
| // Extract colors from image | |
| function extractColors(imageSrc) { | |
| const img = new Image(); | |
| img.src = imageSrc; | |
| img.onload = function() { | |
| const canvas = document.createElement('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = img.width; | |
| canvas.height = img.height; | |
| ctx.drawImage(img, 0, 0); | |
| // Get image data | |
| const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = imageData.data; | |
| // Simple color extraction (top-left and bottom-right corners) | |
| const pixel1 = (0 * 4) + (0 * canvas.width * 4); | |
| const pixel2 = ((canvas.height - 1) * canvas.width * 4) + ((canvas.width - 1) * 4); | |
| const color1Value = `rgb(${data[pixel1]}, ${data[pixel1 + 1]}, ${data[pixel1 + 2]})`; | |
| const color2Value = `rgb(${data[pixel2]}, ${data[pixel2 + 1]}, ${data[pixel2 + 2]})`; | |
| color1.style.backgroundColor = color1Value; | |
| color2.style.backgroundColor = color2Value; | |
| }; | |
| } | |
| // Handle 3D model upload | |
| modelFileInput.addEventListener('change', function(e) { | |
| if (e.target.files && e.target.files[0]) { | |
| const file = e.target.files[0]; | |
| const url = URL.createObjectURL(file); | |
| // Load model in main viewer | |
| loadModel(url, scene, renderer, camera, (loadedModel) => { | |
| // Remove previous model | |
| if (model) { | |
| scene.remove(model); | |
| } | |
| model = loadedModel; | |
| scene.add(model); | |
| // Hide placeholder and show canvas | |
| modelPlaceholder.classList.add('hidden'); | |
| threedCanvas.classList.remove('hidden'); | |
| }); | |
| // Load model in preview viewer | |
| loadModel(url, previewScene, previewRenderer, previewCamera, (loadedModel) => { | |
| // Remove previous model | |
| if (previewModel) { | |
| previewScene.remove(previewModel); | |
| } | |
| previewModel = loadedModel; | |
| previewScene.add(previewModel); | |
| // Show preview model | |
| previewModelPlaceholder.classList.add('hidden'); | |
| previewThreedCanvas.classList.remove('hidden'); | |
| }); | |
| } | |
| }); | |
| // Load GLTF/GLB model | |
| function loadModel(url, scene, renderer, camera, onLoad) { | |
| const loader = new GLTFLoader(); | |
| loader.load(url, (gltf) => { | |
| const model = gltf.scene; | |
| // Center and scale model | |
| const box = new THREE.Box3().setFromObject(model); | |
| const center = box.getCenter(new THREE.Vector3()); | |
| const size = box.getSize(new THREE.Vector3()).length(); | |
| model.position.x += (model.position.x - center.x); | |
| model.position.y += (model.position.y - center.y); | |
| model.position.z += (model.position.z - center.z); | |
| const scale = 3 / size; | |
| model.scale.set(scale, scale, scale); | |
| onLoad(model); | |
| }, undefined, (error) => { | |
| console.error('Error loading model:', error); | |
| }); | |
| } | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| // Update main viewer | |
| camera.aspect = threedCanvas.clientWidth / threedCanvas.clientHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(threedCanvas.clientWidth, threedCanvas.clientHeight); | |
| // Update preview viewer | |
| previewCamera.aspect = previewThreedCanvas.clientWidth / previewThreedCanvas.clientHeight; | |
| previewCamera.updateProjectionMatrix(); | |
| previewRenderer.setSize(previewThreedCanvas.clientWidth, previewThreedCanvas.clientHeight); | |
| }); | |
| // Stats slider events | |
| speedSlider.addEventListener('input', function() { | |
| speedValue.textContent = this.value; | |
| previewSpeedValue.textContent = this.value; | |
| previewSpeedBar.style.width = this.value + '%'; | |
| }); | |
| handlingSlider.addEventListener('input', function() { | |
| handlingValue.textContent = this.value; | |
| previewHandlingValue.textContent = this.value; | |
| previewHandlingBar.style.width = this.value + '%'; | |
| }); | |
| accelerationSlider.addEventListener('input', function() { | |
| accelerationValue.textContent = this.value; | |
| previewAccelerationValue.textContent = this.value; | |
| previewAccelerationBar.style.width = this.value + '%'; | |
| }); | |
| reliabilitySlider.addEventListener('input', function() { | |
| reliabilityValue.textContent = this.value; | |
| previewReliabilityValue.textContent = this.value; | |
| previewReliabilityBar.style.width = this.value + '%'; | |
| }); | |
| // Apply colors button | |
| applyColorsBtn.addEventListener('click', function() { | |
| const primaryColor = color1.style.backgroundColor; | |
| const secondaryColor = color2.style.backgroundColor; | |
| // Update preview card colors | |
| document.querySelector('.racer-card').style.borderColor = primaryColor; | |
| document.querySelector('.stat-fill').style.background = `linear-gradient(90deg, ${primaryColor}, ${secondaryColor})`; | |
| }); | |
| // Create Racer Card Functionality | |
| document.getElementById('create-card-btn').addEventListener('click', function() { | |
| // Get form values | |
| const racerName = document.getElementById('racer-name').value || 'Racer Name'; | |
| const team = document.getElementById('team').value || 'Lightning Speed'; | |
| // Update preview card with form values | |
| document.querySelector('.racer-card h3').textContent = racerName; | |
| document.querySelector('.racer-card p').textContent = team; | |
| // Show export buttons | |
| document.getElementById('save-png-btn').classList.remove('hidden'); | |
| document.getElementById('save-webp-btn').classList.remove('hidden'); | |
| document.getElementById('save-jpg-btn').classList.remove('hidden'); | |
| // If a 3D model is loaded, ensure it's visible in the preview | |
| if (previewModel) { | |
| previewModelPlaceholder.classList.add('hidden'); | |
| previewThreedCanvas.classList.remove('hidden'); | |
| } | |
| // Scroll to preview | |
| document.querySelector('.sticky').scrollIntoView({ behavior: 'smooth' }); | |
| }); | |
| // Save as PNG | |
| document.getElementById('save-png-btn').addEventListener('click', function() { | |
| saveCardAsImage('png'); | |
| }); | |
| // Save as WebP | |
| document.getElementById('save-webp-btn').addEventListener('click', function() { | |
| saveCardAsImage('webp'); | |
| }); | |
| // Save as JPG | |
| document.getElementById('save-jpg-btn').addEventListener('click', function() { | |
| saveCardAsImage('jpeg'); | |
| }); | |
| // Function to save card as image | |
| function saveCardAsImage(format) { | |
| const card = document.querySelector('.racer-card'); | |
| // Temporarily add styles for proper rendering | |
| const originalPosition = card.style.position; | |
| const originalZIndex = card.style.zIndex; | |
| card.style.position = 'relative'; | |
| card.style.zIndex = '1000'; | |
| html2canvas(card, { | |
| backgroundColor: null, | |
| scale: 2 | |
| }).then(canvas => { | |
| // Restore original styles | |
| card.style.position = originalPosition; | |
| card.style.zIndex = originalZIndex; | |
| // Create download link | |
| const link = document.createElement('a'); | |
| const racerName = document.querySelector('.racer-card h3').textContent || 'racer-card'; | |
| link.download = `${racerName}-card.${format}`; | |
| // Convert to data URL based on format | |
| if (format === 'jpeg') { | |
| link.href = canvas.toDataURL('image/jpeg', 0.9); | |
| } else if (format === 'webp') { | |
| link.href = canvas.toDataURL('image/webp', 0.9); | |
| } else { | |
| link.href = canvas.toDataURL('image/png'); | |
| } | |
| // Trigger download | |
| link.click(); | |
| }); | |
| } | |
| </script> | |
| <!-- Add html2canvas library for image export --> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js"></script> | |
| </body> | |
| </html> | |