Shop integration

This commit is contained in:
2026-01-14 17:47:58 +01:00
parent be7f7b7bf7
commit 21b78f8d17
52 changed files with 5288 additions and 198 deletions

View 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}")

View 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
View 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>

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
parallax-demo/workshop.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB