<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POG Generator</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvas-to-blob/1.0.0/canvas-to-blob.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
background: #1a1a1a;
display: flex;
justify-content: flex-start;
align-items: center;
height: 100vh;
font-family: Arial, sans-serif;
}
#canvas {
position: fixed;
top: 0;
left: 0;
width: calc(100% - 300px);
height: 100%;
}
.controls {
position: fixed;
right: 0;
top: 0;
width: 300px;
height: 100vh;
background: rgba(255, 255, 255, 0.1);
padding: 20px;
box-sizing: border-box;
backdrop-filter: blur(10px);
color: white;
overflow-y: auto;
}
input[type="file"] {
width: 100%;
margin: 10px 0;
background: rgba(255, 255, 255, 0.1);
padding: 8px;
border-radius: 5px;
color: white;
}
input[type="range"] {
width: 100%;
margin: 10px 0;
}
.speed-control {
margin: 15px 0;
padding: 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 5px;
}
.speed-value {
color: #4CAF50;
font-weight: bold;
}
h3 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
button {
background: #4CAF50;
border: none;
padding: 10px 20px;
color: white;
border-radius: 5px;
cursor: pointer;
margin: 5px;
transition: all 0.3s ease;
}
button:hover {
background: #45a049;
}
button.active {
background: #f44336;
}
button.active:hover {
background: #d32f2f;
}
.export-button {
background: #2196F3;
margin-top: 20px;
}
.export-button:hover {
background: #1976D2;
}
.export-button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<div id="canvas"></div>
<div class="controls">
<h3>POG Generator</h3>
<div>
<label>Front Face:</label>
<input type="file" id="frontImage" accept="image/*">
</div>
<div>
<label>Back Face:</label>
<input type="file" id="backImage" accept="image/*">
</div>
<div class="speed-control">
<label>Spin Speed: <span class="speed-value">0.01</span></label>
<input type="range" id="speedSlider" min="0.01" max="0.2" step="0.01" value="0.01">
</div>
<button id="spinButton">Spin Coin</button>
<button id="exportButton" class="export-button">Export Frames</button>
</div>
<script>
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('canvas').appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const pointLight1 = new THREE.PointLight(0xffffff, 1);
pointLight1.position.set(3, 2, 0);
scene.add(pointLight1);
const pointLight2 = new THREE.PointLight(0xffffff, 0.8);
pointLight2.position.set(-5, 2, 0);
scene.add(pointLight2);
const pointLight3 = new THREE.PointLight(0xffffff, 0.5);
pointLight3.position.set(0, 5, 0);
scene.add(pointLight3);
const geometry = new THREE.CylinderGeometry(2, 2, 0.1, 32);
const material = new THREE.MeshStandardMaterial({
metalness: 0.3,
roughness: 0.2,
color: 0xffffff
});
const coin = new THREE.Mesh(geometry, material);
coin.rotation.x = Math.PI / 2;
scene.add(coin);
// Position camera
camera.position.z = 5;
camera.position.y = 2;
camera.lookAt(0, 0, 0);
let isSpinning = true;
let spinSpeed = 0.01;
const speedSlider = document.getElementById('speedSlider');
const speedValue = document.querySelector('.speed-value');
speedSlider.addEventListener('input', (e) => {
spinSpeed = parseFloat(e.target.value);
speedValue.textContent = spinSpeed.toFixed(2);
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
const spinButton = document.getElementById('spinButton');
spinButton.addEventListener('click', () => {
isSpinning = !isSpinning;
spinButton.textContent = isSpinning ? 'Stop Coin' : 'Spin Coin';
spinButton.classList.toggle('active');
});
function loadTexture(file, side) {
const reader = new FileReader();
reader.onload = function(e) {
const texture = new THREE.TextureLoader().load(e.target.result);
texture.needsUpdate = true;
texture.rotation = Math.PI / 2;
texture.center.set(0.5, 0.5);
const sideMaterial = new THREE.MeshStandardMaterial({
map: texture,
metalness: 0.3,
roughness: 0.2,
color: 0xffffff
});
let materials;
if (Array.isArray(coin.material)) {
materials = [...coin.material];
} else {
materials = [
new THREE.MeshStandardMaterial({ color: 0x888888 }), // side
new THREE.MeshStandardMaterial({ color: 0xffffff }), // top
new THREE.MeshStandardMaterial({ color: 0xffffff }) // bottom
];
}
if (side === 'front') {
materials[1] = sideMaterial; // top
} else if (side === 'back') {
materials[2] = sideMaterial; // bottom
}
coin.material = materials;
};
reader.readAsDataURL(file);
}
document.getElementById('frontImage').addEventListener('change', (e) => {
if (e.target.files[0]) {
loadTexture(e.target.files[0], 'front');
}
});
document.getElementById('backImage').addEventListener('change', (e) => {
if (e.target.files[0]) {
loadTexture(e.target.files[0], 'back');
}
});
function animate() {
requestAnimationFrame(animate);
if (isSpinning) {
coin.rotation.z += spinSpeed;
}
controls.update();
renderer.render(scene, camera);
}
animate();
// GIF Export functionality
const exportButton = document.getElementById('exportButton');
let isRecording = false;
let frames = [];
const frameCount = 90; // 3 seconds at 30fps
let currentFrame = 0;
exportButton.addEventListener('click', () => {
if (isRecording) return;
isRecording = true;
exportButton.disabled = true;
exportButton.textContent = 'Recording...';
frames = [];
currentFrame = 0;
// Create a temporary canvas for capturing frames
const tempCanvas = document.createElement('canvas');
tempCanvas.width = window.innerWidth - 300;
tempCanvas.height = window.innerHeight;
const tempContext = tempCanvas.getContext('2d');
// Start recording frames
function captureFrame() {
if (currentFrame < frameCount) {
// Render the current frame
renderer.render(scene, camera);
// Copy WebGL canvas to temporary canvas
tempContext.drawImage(renderer.domElement, 0, 0);
// Store the frame as a data URL
frames.push(tempCanvas.toDataURL('image/png'));
currentFrame++;
requestAnimationFrame(captureFrame);
} else {
// Create a zip file containing all frames
const zip = new JSZip();
// Add all frames to the zip
frames.forEach((frame, index) => {
// Convert data URL to blob
const base64Data = frame.split(',')[1];
zip.file(`frame_${index.toString().padStart(3, '0')}.png`, base64Data, {base64: true});
});
// Generate the zip file
zip.generateAsync({type: 'blob'}).then(function(content) {
// Create download link
const url = URL.createObjectURL(content);
const a = document.createElement('a');
a.href = url;
a.download = 'pog-coin-frames.zip';
a.click();
URL.revokeObjectURL(url);
// Reset button
exportButton.disabled = false;
exportButton.textContent = 'Export Frames';
isRecording = false;
});
}
}
captureFrame();
});
</script>
</body>
</html>