Shop integration
This commit is contained in:
303
parallax-demo/index.html
Normal file
303
parallax-demo/index.html
Normal file
@@ -0,0 +1,303 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>3D Parallax Workshop Demo</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
|
||||
<style>
|
||||
body { margin: 0; overflow-x: hidden; background-color: #0f0f0f; font-family: sans-serif; color: white; }
|
||||
|
||||
.spacer {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 { font-size: 3rem; margin-bottom: 2rem; }
|
||||
p { max-width: 600px; line-height: 1.6; color: #aaa; }
|
||||
|
||||
.parallax-section {
|
||||
position: relative;
|
||||
height: 400vh; /* Scroll distance */
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.sticky-wrapper {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#parallax-canvas {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.product-reveal {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
width: 60%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.product-reveal img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
filter: drop-shadow(0 20px 40px rgba(0,0,0,0.5));
|
||||
}
|
||||
|
||||
/* Loading Overlay */
|
||||
#loader {
|
||||
position: fixed; inset: 0; background: #0f0f0f; z-index: 999;
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="spacer">
|
||||
<h1>Scroll Down</h1>
|
||||
<p>Experience the journey from the workshop to the finished form.</p>
|
||||
</section>
|
||||
|
||||
<section class="parallax-section">
|
||||
<div class="sticky-wrapper">
|
||||
<canvas id="parallax-canvas"></canvas>
|
||||
<div class="product-reveal">
|
||||
<img src="pottery-vase.png" id="final-img">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="spacer">
|
||||
<h1>Collection 014</h1>
|
||||
<p>Every piece tells a story.</p>
|
||||
</section>
|
||||
|
||||
<div id="loader">Loading Assets...</div>
|
||||
|
||||
<script>
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
async function init() {
|
||||
const canvas = document.querySelector('#parallax-canvas');
|
||||
|
||||
// THREE SETUP
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
|
||||
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
|
||||
// CHECK PROTOCOL
|
||||
if (window.location.protocol === 'file:') {
|
||||
const loader = document.getElementById('loader');
|
||||
loader.innerHTML = `
|
||||
<div style="text-align:center; padding: 2rem;">
|
||||
<h2 style="color: #ff6b6b">File Protocol Error</h2>
|
||||
<p>Browsers cannot load textures directly from local files due to security restrictions.</p>
|
||||
<p style="margin-top: 1rem; font-weight: bold; color: white;">Please open <a href="index_embedded.html" style="color: #4cd137">index_embedded.html</a> instead.</p>
|
||||
<p style="font-size: 0.8em; color: #888">Or use a local server (e.g. python -m http.server).</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// LOAD TEXTURES
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
const loadTexture = (url) => new Promise((resolve, reject) => {
|
||||
textureLoader.load(url, resolve, undefined, reject);
|
||||
});
|
||||
|
||||
try {
|
||||
// Load assets for both Workshop and Vase
|
||||
const [workshopTex, workshopDepth, vaseTex, vaseDepth] = await Promise.all([
|
||||
loadTexture('workshop.jpg'),
|
||||
loadTexture('workshop_depth.png'),
|
||||
loadTexture('pottery-vase.png'),
|
||||
loadTexture('pottery-vase_depth.png')
|
||||
]);
|
||||
|
||||
document.getElementById('loader').style.display = 'none';
|
||||
|
||||
// --- IMAGE CONSTANTS ---
|
||||
const WORKSHOP_ASPECT = workshopTex.image.width / workshopTex.image.height;
|
||||
const VASE_ASPECT = vaseTex.image.width / vaseTex.image.height;
|
||||
|
||||
// --- SHADER SETUP ---
|
||||
const vertexShader = `
|
||||
varying vec2 vUv;
|
||||
uniform sampler2D uDepth;
|
||||
uniform float uDepthScale;
|
||||
uniform vec2 uMouse;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
float depth = texture2D(uDepth, uv).r;
|
||||
vec3 pos = position;
|
||||
|
||||
// Z Displacement
|
||||
pos.z += depth * uDepthScale;
|
||||
|
||||
// Mouse Parallax (Low intensity)
|
||||
pos.x += (uMouse.x * depth * 0.02);
|
||||
pos.y += (uMouse.y * depth * 0.02);
|
||||
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShader = `
|
||||
varying vec2 vUv;
|
||||
uniform sampler2D uImage;
|
||||
uniform float uOpacity;
|
||||
|
||||
void main() {
|
||||
vec4 color = texture2D(uImage, vUv);
|
||||
gl_FragColor = vec4(color.rgb, color.a * uOpacity);
|
||||
}
|
||||
`;
|
||||
|
||||
// --- WORKSHOP MESH ---
|
||||
// Geometry matches image aspect ratio (Width, 1.0)
|
||||
const workshopGeo = new THREE.PlaneGeometry(WORKSHOP_ASPECT, 1, 128, 128);
|
||||
const workshopMat = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uImage: { value: workshopTex },
|
||||
uDepth: { value: workshopDepth },
|
||||
uMouse: { value: new THREE.Vector2(0, 0) },
|
||||
uDepthScale: { value: 0.15 },
|
||||
uOpacity: { value: 1.0 }
|
||||
},
|
||||
vertexShader,
|
||||
fragmentShader,
|
||||
transparent: true
|
||||
});
|
||||
const workshopMesh = new THREE.Mesh(workshopGeo, workshopMat);
|
||||
scene.add(workshopMesh);
|
||||
|
||||
// --- VASE MESH ---
|
||||
const vaseGeo = new THREE.PlaneGeometry(VASE_ASPECT, 1, 128, 128);
|
||||
const vaseMat = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uImage: { value: vaseTex },
|
||||
uDepth: { value: vaseDepth },
|
||||
uMouse: { value: new THREE.Vector2(0, 0) },
|
||||
uDepthScale: { value: 0.15 },
|
||||
uOpacity: { value: 0.0 }
|
||||
},
|
||||
vertexShader,
|
||||
fragmentShader,
|
||||
transparent: true
|
||||
});
|
||||
const vaseMesh = new THREE.Mesh(vaseGeo, vaseMat);
|
||||
vaseMesh.position.z = 0.1; // Just in front to avoid z-fighting
|
||||
scene.add(vaseMesh);
|
||||
|
||||
|
||||
// Camera Start
|
||||
const DISTANCE = 4.0;
|
||||
camera.position.z = DISTANCE;
|
||||
|
||||
// Handle Resize & COVER/CONTAIN Logic
|
||||
const handleResize = () => {
|
||||
const screenAspect = window.innerWidth / window.innerHeight;
|
||||
|
||||
camera.aspect = screenAspect;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
|
||||
// Calculate Visible Height at the mesh distance
|
||||
// fov is vertical fov in degrees
|
||||
const vFOV = camera.fov * Math.PI / 180;
|
||||
const visibleHeight = 2 * Math.tan(vFOV / 2) * (DISTANCE - workshopMesh.position.z);
|
||||
const visibleWidth = visibleHeight * screenAspect;
|
||||
|
||||
// 1. WORKSHOP: COVER
|
||||
// We want the mesh (which is Aspect x 1) to cover VisibleWidth x VisibleHeight
|
||||
// Scale X and Y by the same factor to maintain aspect
|
||||
const scaleFactorCover = Math.max(visibleWidth / WORKSHOP_ASPECT, visibleHeight / 1);
|
||||
workshopMesh.scale.set(scaleFactorCover, scaleFactorCover, 1);
|
||||
|
||||
// 2. VASE: CONTAIN / SAFE COVER
|
||||
// We want it visible. Let's make it cover 80% of min dimension, or standard scale.
|
||||
// Let's just fit it to height generally, or cover if desired.
|
||||
// User said "rotate... explore", let's make it fairly large but contained.
|
||||
const scaleFactorContain = Math.min(visibleWidth / VASE_ASPECT, visibleHeight / 1) * 0.8;
|
||||
vaseMesh.scale.set(scaleFactorContain, scaleFactorContain, 1);
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize();
|
||||
|
||||
// ANIMATION LOOP
|
||||
const mouse = new THREE.Vector2(0, 0);
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
workshopMat.uniforms.uMouse.value.lerp(mouse, 0.05);
|
||||
vaseMat.uniforms.uMouse.value.lerp(mouse, 0.05);
|
||||
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
animate();
|
||||
|
||||
// INTERACTIONS
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
|
||||
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
||||
});
|
||||
|
||||
// Scroll Animation
|
||||
const tl = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: ".parallax-section",
|
||||
start: "top top",
|
||||
end: "bottom bottom",
|
||||
scrub: true
|
||||
}
|
||||
});
|
||||
|
||||
// Transition
|
||||
// Workshop Fades Out
|
||||
tl.to(workshopMat.uniforms.uOpacity, { value: 0, ease: "power1.out" }, 0.2);
|
||||
|
||||
// Vase Fades In and Zooms slightly
|
||||
tl.to(vaseMat.uniforms.uOpacity, { value: 1, ease: "power1.in" }, 0.2);
|
||||
|
||||
// Camera move? Maybe subtle
|
||||
tl.to(camera.position, { z: 3.5, ease: "none" }, 0);
|
||||
|
||||
// Vase rotation/movement
|
||||
tl.fromTo(vaseMesh.rotation, { z: -0.05 }, { z: 0.05, ease: "none"}, 0.2);
|
||||
|
||||
// Hide loader/overlay if any
|
||||
tl.to(".product-reveal", { opacity: 0, duration: 0 }, 0);
|
||||
tl.to(canvas, { opacity: 1 }, 0);
|
||||
|
||||
} catch (err) {
|
||||
console.error("Error loading assets:", err);
|
||||
document.getElementById('loader').innerText = "Error loading assets: " + err.message;
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user