Skip to content

Instantly share code, notes, and snippets.

@bfollington
Created September 4, 2024 06:33
Show Gist options
  • Save bfollington/4080172841cc5f24ef7fc0c5acf472c1 to your computer and use it in GitHub Desktop.
Save bfollington/4080172841cc5f24ef7fc0c5acf472c1 to your computer and use it in GitHub Desktop.
Sticker Interaction
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Holographic Stickers with Improved Lighting</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<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.9.1/gsap.min.js"></script>
<script>
let camera, scene, renderer, raycaster, mouse, floor;
let draggingSticker = null;
const stickers = [];
let lastMousePosition = new THREE.Vector2();
let mouseVelocity = new THREE.Vector2();
const holographicVertexShader = `
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vUv = uv;
vNormal = normalize(normalMatrix * normal);
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vViewPosition = -mvPosition.xyz;
gl_Position = projectionMatrix * mvPosition;
}
`;
const holographicFragmentShader = `
uniform vec3 baseColor;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vViewPosition;
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187, 0.366025403784439,
-0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m ;
m = m*m ;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDir = normalize(vViewPosition);
float fresnelTerm = dot(normal, viewDir);
fresnelTerm = clamp(1.0 - fresnelTerm, 0.0, 1.0);
fresnelTerm = pow(fresnelTerm, 3.0);
float noiseValue = snoise(vUv * 10.0);
vec3 rainbow = 0.5 + 0.5 * cos(2.0 * 3.14159 * (vec3(0.0, 0.33, 0.67) + fresnelTerm + noiseValue));
vec3 reflection = normalize(reflect(-viewDir, normal));
float specular = max(0.0, dot(normal, reflection));
specular = pow(specular, 20.0);
gl_FragColor = vec4(mix(baseColor, rainbow, 0.8) + specular, 1.0);
}
`;
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 10, 10);
camera.lookAt(0, 0, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Ambient light
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
// Cross-angled, colored lights
const light1 = new THREE.DirectionalLight(0xffcccc, 0.5); // Warm light
light1.position.set(5, 10, 5);
light1.castShadow = true;
scene.add(light1);
const light2 = new THREE.DirectionalLight(0xccccff, 0.5); // Cool light
light2.position.set(-5, 10, -5);
light2.castShadow = true;
scene.add(light2);
floor = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.MeshPhongMaterial({ color: 0x222222 })
);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
createCircleSticker(-3, 0.01, 0, new THREE.Color(0xff0000));
createTriangleSticker(0, 0.01, 0, new THREE.Color(0x00ff00));
createDiamondSticker(3, 0.01, 0, new THREE.Color(0x0000ff));
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mousedown', onMouseDown);
window.addEventListener('mouseup', onMouseUp);
window.addEventListener('resize', onWindowResize);
}
function createHolographicMaterial(baseColor) {
return new THREE.ShaderMaterial({
uniforms: {
baseColor: { value: baseColor }
},
vertexShader: holographicVertexShader,
fragmentShader: holographicFragmentShader,
side: THREE.DoubleSide
});
}
function createCircleSticker(x, y, z, color) {
const geometry = new THREE.CircleGeometry(1, 32);
const material = createHolographicMaterial(color);
createSticker(geometry, material, x, y, z);
}
function createTriangleSticker(x, y, z, color) {
const shape = new THREE.Shape();
shape.moveTo(0, 1);
shape.lineTo(-1, -1);
shape.lineTo(1, -1);
shape.lineTo(0, 1);
const geometry = new THREE.ShapeGeometry(shape);
const material = createHolographicMaterial(color);
createSticker(geometry, material, x, y, z);
}
function createDiamondSticker(x, y, z, color) {
const shape = new THREE.Shape();
shape.moveTo(0, 1);
shape.lineTo(1, 0);
shape.lineTo(0, -1);
shape.lineTo(-1, 0);
shape.lineTo(0, 1);
const geometry = new THREE.ShapeGeometry(shape);
const material = createHolographicMaterial(color);
createSticker(geometry, material, x, y, z);
}
function createSticker(geometry, material, x, y, z) {
const sticker = new THREE.Mesh(geometry, material);
sticker.position.set(x, y, z);
sticker.rotation.x = -Math.PI / 2;
sticker.castShadow = true;
sticker.receiveShadow = true;
scene.add(sticker);
stickers.push(sticker);
}
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
mouseVelocity.x = mouse.x - lastMousePosition.x;
mouseVelocity.y = mouse.y - lastMousePosition.y;
lastMousePosition.copy(mouse);
if (draggingSticker) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(floor);
if (intersects.length > 0) {
const targetPosition = intersects[0].point.clone();
targetPosition.y = 1;
gsap.to(draggingSticker.position, {
x: targetPosition.x,
y: targetPosition.y,
z: targetPosition.z,
duration: 0.3,
ease: "power2.out"
});
// Wobbly spring effect
const springStrength = 0.05;
const dampingFactor = 0.8;
const tiltX = -mouseVelocity.y * 2 * springStrength;
const tiltZ = mouseVelocity.x * 2 * springStrength;
gsap.to(draggingSticker.rotation, {
x: -Math.PI / 2 + tiltX,
z: tiltZ,
duration: 0.5,
ease: "elastic.out(1.5, 0.3)"
});
}
}
}
function onMouseDown(event) {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(stickers);
if (intersects.length > 0) {
draggingSticker = intersects[0].object;
gsap.to(draggingSticker.position, {
y: 1,
duration: 0.2,
ease: "back.out(2)"
});
}
}
function onMouseUp() {
if (draggingSticker) {
gsap.to(draggingSticker.position, {
y: 0.01,
duration: 0.5,
ease: "bounce.out"
});
gsap.to(draggingSticker.rotation, {
x: -Math.PI / 2,
z: 0,
duration: 0.5,
ease: "power2.out"
});
draggingSticker = null;
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
init();
animate();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment