Shop integration
This commit is contained in:
43
parallax-demo/generate_embedded.py
Normal file
43
parallax-demo/generate_embedded.py
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
import base64
|
||||
import os
|
||||
|
||||
def get_base64_src(path, mime_type):
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
return f"data:{mime_type};base64,{base64.b64encode(data).decode('utf-8')}"
|
||||
|
||||
try:
|
||||
# 1. Read Assets
|
||||
workshop_b64 = get_base64_src("workshop.jpg", "image/jpeg")
|
||||
depth_b64 = get_base64_src("workshop_depth.png", "image/png")
|
||||
vase_b64 = get_base64_src("pottery-vase.png", "image/png")
|
||||
try:
|
||||
vase_depth_b64 = get_base64_src("pottery-vase_depth.png", "image/png")
|
||||
except FileNotFoundError:
|
||||
print("Warning: pottery-vase_depth.png not found. Using placeholder or skipping.")
|
||||
vase_depth_b64 = depth_b64 # Fallback to something to avoid crash
|
||||
|
||||
# 2. Read HTML Template
|
||||
with open("index.html", "r", encoding="utf-8") as f:
|
||||
html = f.read()
|
||||
|
||||
# Remove the protocol check block for the embedded version
|
||||
# because embedded base64 images work fine on file:// protocol
|
||||
import re
|
||||
html = re.sub(r'// CHECK PROTOCOL[\s\S]*?return;\s+?}', '', html)
|
||||
|
||||
# 3. Replace with Base64
|
||||
html = html.replace("workshop.jpg", workshop_b64)
|
||||
html = html.replace("workshop_depth.png", depth_b64)
|
||||
html = html.replace("pottery-vase.png", vase_b64)
|
||||
html = html.replace("pottery-vase_depth.png", vase_depth_b64)
|
||||
|
||||
# 4. Write new file
|
||||
with open("index_embedded.html", "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
|
||||
print("Successfully created index_embedded.html")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
42
parallax-demo/generate_vase_depth.py
Normal file
42
parallax-demo/generate_vase_depth.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
def create_cylindrical_depth_map(image_path, output_path):
|
||||
# Load image
|
||||
img = Image.open(image_path).convert("RGBA")
|
||||
width, height = img.size
|
||||
|
||||
# Create a numpy array for the depth map
|
||||
# We want a gradient that is white in the center horizontal axis and black at the edges (cylindrical)
|
||||
# 0 = far (black), 255 = near (white)
|
||||
|
||||
# Generate X coordinates (0 to width)
|
||||
x = np.linspace(-1, 1, width)
|
||||
# Compute cylindrical depth: sqrt(1 - x^2) for a perfect cylinder, or just a cosine/parabolic falloff
|
||||
# Let's use cosine for smooth roundness: cos(x * pi / 2)
|
||||
depth_profile = np.cos(x * np.pi / 2) # Center (0) is 1, Edges (-1, 1) are 0
|
||||
|
||||
# Normalize to 0-255
|
||||
depth_profile = (depth_profile * 255).astype(np.uint8)
|
||||
|
||||
# Tile vertically to create the full map
|
||||
depth_map = np.tile(depth_profile, (height, 1))
|
||||
|
||||
# Create Image from array
|
||||
depth_img = Image.fromarray(depth_map, mode='L')
|
||||
|
||||
# MASKING: We only want the vase to have depth, the background (transparent) should be flat/far.
|
||||
# Use the alpha channel of the original image as a mask
|
||||
alpha = np.array(img.split()[-1])
|
||||
|
||||
# Where alpha is 0 (background), set depth to 0 (flat/far)
|
||||
depth_array = np.array(depth_img)
|
||||
depth_array[alpha < 10] = 0 # Threshold for transparency
|
||||
|
||||
# Save
|
||||
final_depth = Image.fromarray(depth_array)
|
||||
final_depth.save(output_path)
|
||||
print(f"Generated depth map: {output_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_cylindrical_depth_map("pottery-vase.png", "pottery-vase_depth.png")
|
||||
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>
|
||||
291
parallax-demo/index_embedded.html
Normal file
291
parallax-demo/index_embedded.html
Normal file
File diff suppressed because one or more lines are too long
BIN
parallax-demo/pottery-vase.png
Normal file
BIN
parallax-demo/pottery-vase.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 701 KiB |
BIN
parallax-demo/pottery-vase_depth.png
Normal file
BIN
parallax-demo/pottery-vase_depth.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
BIN
parallax-demo/workshop.jpg
Normal file
BIN
parallax-demo/workshop.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 359 KiB |
BIN
parallax-demo/workshop_depth.png
Normal file
BIN
parallax-demo/workshop_depth.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 490 KiB |
Reference in New Issue
Block a user