wavetype-xyz-index2 / create.racer-profile.html
vgrowhouse's picture
add <!DOCTYPE html>
deaf3ce verified
raw
history blame
37.8 kB
<!DOCTYPE html>
<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>