<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>홈 on 내 블로그</title><link>https://gromit.net/</link><description>Recent content in 홈 on 내 블로그</description><generator>Hugo</generator><language>ko-kr</language><lastBuildDate>Thu, 09 Apr 2026 18:40:00 +0900</lastBuildDate><atom:link href="https://gromit.net/index.xml" rel="self" type="application/rss+xml"/><item><title>Parallax with Depth Map</title><link>https://gromit.net/parallax/</link><pubDate>Thu, 09 Apr 2026 18:40:00 +0900</pubDate><guid>https://gromit.net/parallax/</guid><description>&lt;style&gt;
#parallax-container {
width: 100%;
max-width: 800px;
margin: 40px auto;
position: relative;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
#parallax-canvas {
width: 100%;
height: auto;
display: block;
cursor: move;
}
#hint {
text-align: center;
margin-top: 20px;
color: #666;
font-size: 14px;
}
&lt;/style&gt;
&lt;div id="parallax-container"&gt;
&lt;canvas id="parallax-canvas"&gt;&lt;/canvas&gt;
&lt;/div&gt;
&lt;div id="hint"&gt;Move your mouse over the image&lt;/div&gt;
&lt;script&gt;
(function() {
const canvas = document.getElementById('parallax-canvas');
const ctx = canvas.getContext('2d');
const hint = document.getElementById('hint');
const colorImg = new Image();
const depthImg = new Image();
let loaded = 0;
let mouseX = 0.5;
let mouseY = 0.5;
let targetX = 0.5;
let targetY = 0.5;
const isDesktop = window.matchMedia('(min-width: 768px)').matches;
const lerpSpeed = isDesktop ? 0.1 : 0.25; // Mobile: faster response
// Load images
colorImg.onload = checkLoaded;
depthImg.onload = checkLoaded;
colorImg.src = '/images/example.webp';
depthImg.src = '/images/example_depth.webp';
function checkLoaded() {
loaded++;
if (loaded === 2) {
setupCanvas();
animate();
}
}
function setupCanvas() {
canvas.width = colorImg.width;
canvas.height = colorImg.height;
canvas.style.width = '100%';
canvas.style.height = 'auto';
}
// Mouse/touch/gyro interaction
if (isDesktop) {
// Desktop: Mouse
canvas.addEventListener('mousemove', (e) =&gt; {
const rect = canvas.getBoundingClientRect();
targetX = (e.clientX - rect.left) / rect.width;
targetY = (e.clientY - rect.top) / rect.height;
});
canvas.addEventListener('mouseleave', () =&gt; {
targetX = 0.5;
targetY = 0.5;
});
} else {
// Mobile: Gyroscope
hint.textContent = 'Tilt your device (tap to enable)';
let gyroActive = false;
let initialBeta = null;
let initialGamma = null;
function handleOrientation(event) {
const beta = event.beta; // X축 회전 (-180 ~ 180)
const gamma = event.gamma; // Y축 회전 (-90 ~ 90)
if (beta === null || gamma === null) return;
// 초기 각도 설정 (캘리브레이션)
if (initialBeta === null) {
initialBeta = beta;
initialGamma = gamma;
return;
}
// 초기 위치 대비 변화량
const deltaBeta = beta - initialBeta;
const deltaGamma = gamma - initialGamma;
// 더 민감한 감도 (-20 ~ +20도 범위를 0 ~ 1로 매핑)
targetX = 0.5 + (deltaGamma / 40);
targetY = 0.5 + (deltaBeta / 40);
// 범위 제한
targetX = Math.max(0, Math.min(1, targetX));
targetY = Math.max(0, Math.min(1, targetY));
}
function enableGyro() {
if (gyroActive) return;
// iOS 13+ 권한 요청
if (typeof DeviceOrientationEvent.requestPermission === 'function') {
DeviceOrientationEvent.requestPermission()
.then(state =&gt; {
if (state === 'granted') {
window.addEventListener('deviceorientation', handleOrientation);
gyroActive = true;
hint.textContent = 'Tilt your device to move';
} else {
hint.textContent = 'Permission denied - Touch and drag instead';
enableTouchDrag();
}
})
.catch((err) =&gt; {
console.error(err);
hint.textContent = 'Gyro not available - Touch and drag instead';
enableTouchDrag();
});
} else {
// Android 및 이전 iOS
window.addEventListener('deviceorientation', handleOrientation);
gyroActive = true;
hint.textContent = 'Tilt your device to move';
}
}
function enableTouchDrag() {
hint.textContent = 'Touch and drag to move';
let isDragging = false;
canvas.addEventListener('touchstart', () =&gt; {
isDragging = true;
});
canvas.addEventListener('touchmove', (e) =&gt; {
if (!isDragging) return;
e.preventDefault();
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
targetX = (touch.clientX - rect.left) / rect.width;
targetY = (touch.clientY - rect.top) / rect.height;
});
canvas.addEventListener('touchend', () =&gt; {
isDragging = false;
targetX = 0.5;
targetY = 0.5;
});
}
// 자이로 시작 (탭 or 자동)
if (window.DeviceOrientationEvent) {
canvas.addEventListener('click', enableGyro, { once: true });
// iOS가 아니면 자동 시작
if (typeof DeviceOrientationEvent.requestPermission !== 'function') {
enableGyro();
}
} else {
// 자이로 없으면 터치 드래그
enableTouchDrag();
}
}
function animate() {
requestAnimationFrame(animate);
// Smooth lerp (faster on mobile)
mouseX += (targetX - mouseX) * lerpSpeed;
mouseY += (targetY - mouseY) * lerpSpeed;
render();
}
function render() {
const width = canvas.width;
const height = canvas.height;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Get depth data
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(depthImg, 0, 0, width, height);
const depthData = tempCtx.getImageData(0, 0, width, height);
// Draw with stronger parallax effect
const offsetX = (mouseX - 0.5) * 120; // Increased from 50 to 120
const offsetY = (mouseY - 0.5) * 120;
// Draw base image
ctx.drawImage(colorImg, 0, 0, width, height);
// Create displacement effect based on depth
const imageData = ctx.getImageData(0, 0, width, height);
const pixels = imageData.data;
const depthPixels = depthData.data;
const newImageData = ctx.createImageData(width, height);
const newPixels = newImageData.data;
for (let y = 0; y &lt; height; y++) {
for (let x = 0; x &lt; width; x++) {
const i = (y * width + x) * 4;
// Get depth value (0-255)
let depth = depthPixels[i] / 255;
// Apply non-linear curve for stronger depth perception
depth = Math.pow(depth, 1.5); // Stronger depth gradient
// Calculate offset based on depth with enhanced effect
const dx = Math.round(offsetX * depth);
const dy = Math.round(offsetY * depth);
// Source pixel position
const sx = Math.max(0, Math.min(width - 1, x - dx));
const sy = Math.max(0, Math.min(height - 1, y - dy));
const si = (sy * width + sx) * 4;
// Copy pixel
newPixels[i] = pixels[si];
newPixels[i + 1] = pixels[si + 1];
newPixels[i + 2] = pixels[si + 2];
newPixels[i + 3] = pixels[si + 3];
}
}
ctx.putImageData(newImageData, 0, 0);
}
})();
&lt;/script&gt;</description></item><item><title>Picture</title><link>https://gromit.net/picture/</link><pubDate>Thu, 09 Apr 2026 13:42:00 +0900</pubDate><guid>https://gromit.net/picture/</guid><description>&lt;div style="perspective: 1000px; display: flex; justify-content: center; margin: 40px 0;"&gt;
&lt;img id="perspective-img" src="https://gromit.net/images/example.webp" alt="Example Image" style="transform: rotateY(0deg) rotateX(0deg); box-shadow: 20px 20px 40px rgba(0,0,0,0.3); max-width: 600px; width: 100%; border-radius: 8px; transition: transform 0.1s ease-out;"&gt;
&lt;/div&gt;
&lt;script&gt;
(function() {
const img = document.getElementById('perspective-img');
let isDesktop = window.matchMedia('(min-width: 768px)').matches;
// 모바일: 초기 각도 저장
let initialGamma = null;
let initialBeta = null;
let calibrated = false;
// 데스크톱: 마우스 이벤트
if (isDesktop) {
document.addEventListener('mousemove', function(e) {
const x = e.clientX / window.innerWidth;
const y = e.clientY / window.innerHeight;
const rotateY = (x - 0.5) * 60; // -30도 ~ 30도
const rotateX = (y - 0.5) * -40; // -20도 ~ 20도
img.style.transform = `rotateY(${rotateY}deg) rotateX(${rotateX}deg)`;
});
}
// 모바일: 자이로센서
else {
if (window.DeviceOrientationEvent) {
// iOS 13+ 권한 요청
if (typeof DeviceOrientationEvent.requestPermission === 'function') {
img.addEventListener('click', function() {
DeviceOrientationEvent.requestPermission()
.then(permissionState =&gt; {
if (permissionState === 'granted') {
window.addEventListener('deviceorientation', handleOrientation);
}
})
.catch(console.error);
}, { once: true });
} else {
// Android 및 이전 iOS
window.addEventListener('deviceorientation', handleOrientation);
}
}
}
function handleOrientation(event) {
const beta = event.beta; // X축 회전 (-180 ~ 180)
const gamma = event.gamma; // Y축 회전 (-90 ~ 90)
// 초기 각도 캘리브레이션 (처음 한 번만)
if (!calibrated) {
initialGamma = gamma;
initialBeta = beta;
calibrated = true;
return; // 첫 프레임은 스킵
}
// 초기 각도 대비 변화량 계산
let deltaGamma = gamma - initialGamma;
let deltaBeta = beta - initialBeta;
// Deadzone: 작은 움직임 무시 (±2도 이내)
const deadzone = 2;
if (Math.abs(deltaGamma) &lt; deadzone) deltaGamma = 0;
if (Math.abs(deltaBeta) &lt; deadzone) deltaBeta = 0;
// ±30도 범위로 제한하고 감도 적용 (좌우 반전)
const rotateY = Math.max(-30, Math.min(30, -deltaGamma * 0.8));
const rotateX = Math.max(-20, Math.min(20, deltaBeta * 0.5));
img.style.transform = `rotateY(${rotateY}deg) rotateX(${rotateX}deg)`;
}
})();
&lt;/script&gt;</description></item></channel></rss>