Compare commits

..

4 Commits

Author SHA1 Message Date
travis
20b4ec5327 Make card more prominent and fix iOS touch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:16:10 -04:00
travis
595a890704 Update 2026-04-02 16:12:40 -04:00
travis
2f0c937248 Replace Plausible with Umami analytics 2026-03-05 08:58:00 -05:00
travis
a9604a8dc2 Add Umami analytics tracking 2026-03-05 08:57:17 -05:00
3 changed files with 174 additions and 106 deletions

View File

@@ -4,7 +4,9 @@
html, body { html, body {
height: 100%; height: 100%;
height: 100dvh;
min-height: 100%; min-height: 100%;
min-height: 100dvh;
overflow: hidden; overflow: hidden;
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -12,7 +14,6 @@ html, body {
body { body {
background-color: #0e0e0e; background-color: #0e0e0e;
margin: 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -40,9 +41,9 @@ body {
.card { .card {
display: block; display: block;
position: relative; position: relative;
max-width: 420px; max-width: 500px;
width: 80%; width: 85%;
height: 260px; height: 300px;
color: #fff; color: #fff;
flex: none; flex: none;
perspective: 1000px; perspective: 1000px;
@@ -52,10 +53,11 @@ body {
margin: 0; margin: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 100%;
display: block; display: block;
position: relative; position: relative;
background: #111; background: linear-gradient(145deg, #161616, #0e0e0e);
border-radius: 3px; border-radius: 5px;
} }
.card__figure::before { .card__figure::before {
@@ -65,10 +67,10 @@ body {
height: calc(100% - 14px); height: calc(100% - 14px);
top: 6px; top: 6px;
left: 6px; left: 6px;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 2px; border-radius: 3px;
pointer-events: none; pointer-events: none;
box-shadow: 0 30px 20px rgb(0 0 0 / 50%); box-shadow: 0 30px 40px rgb(0 0 0 / 60%), 0 0 80px rgb(0 0 0 / 30%);
} }
.card__deco { .card__deco {
@@ -98,7 +100,7 @@ body {
figcaption { figcaption {
height: 100%; height: 100%;
padding: 25px; padding: 30px;
color: #eee; color: #eee;
font-family: Avenir; font-family: Avenir;
display: flex; display: flex;
@@ -108,9 +110,10 @@ figcaption {
h1 { h1 {
margin: 0; margin: 0;
font-size: 3.3rem; font-size: 3.6rem;
line-height: 3.3rem; line-height: 3.6rem;
font-weight: 900; font-weight: 900;
letter-spacing: -0.02em;
} }
.links { .links {
@@ -123,17 +126,17 @@ h1 {
a { a {
color: #ddd; color: #ddd;
transition: color 100ms ease; transition: color 100ms ease;
height: 20px; display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
} }
a:hover { a:hover {
color: #eee; color: #eee;
} }
p {
margin: 0;
}
svg { svg {
width: 20px; width: 20px;
height: 20px; height: 20px;
@@ -152,7 +155,14 @@ svg {
@media screen and (max-width: 430px) { @media screen and (max-width: 430px) {
.card { .card {
height: 450px; width: 85%;
height: auto;
aspect-ratio: 3 / 4;
max-height: 70dvh;
}
.links span {
font-size: 0.85rem;
} }
} }

View File

@@ -2,7 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Travis Heinström</title> <title>Travis Heinström</title>
@@ -51,9 +50,9 @@
<script src="./js/main.js"></script> <script src="./js/main.js"></script>
<script> <script>
(function () { (function () {
new TiltFx(document.querySelector(".card", {})); new TiltFx(document.querySelector(".card"), {});
})(); })();
</script> </script>
<script defer data-domain="heinstrom.com" src="https://plausible.nyc.h.rip/js/script.js"></script> <script defer src="https://a.log.rip/script.js" data-website-id="1e9e073c-dd8c-4dfd-b939-5066f4239877"></script>
</body> </body>
</html> </html>

View File

@@ -1,7 +1,6 @@
(function (window) { (function (window) {
"use strict"; "use strict";
// Helper vars and functions.
function extend(a, b) { function extend(a, b) {
for (var key in b) { for (var key in b) {
if (b.hasOwnProperty(key)) { if (b.hasOwnProperty(key)) {
@@ -11,26 +10,6 @@
return a; return a;
} }
function getMousePos(e) {
var posx = 0,
posy = 0;
if (!e) var e = window.event;
if (e.pageX || e.pageY) {
posx = e.pageX;
posy = e.pageY;
} else if (e.clientX || e.clientY) {
posx =
e.clientX +
document.body.scrollLeft +
document.documentElement.scrollLeft;
posy =
e.clientY +
document.body.scrollTop +
document.documentElement.scrollTop;
}
return { x: posx, y: posy };
}
/** /**
* TiltFx obj. * TiltFx obj.
*/ */
@@ -73,7 +52,9 @@
this.DOM.animatable.shine = this.DOM.el.querySelector( this.DOM.animatable.shine = this.DOM.el.querySelector(
".card__deco--shine > div" ".card__deco--shine > div"
); );
this._gyroActive = false;
this._initEvents(); this._initEvents();
this._initGyro();
}; };
/** /**
@@ -89,75 +70,68 @@
}; };
this.mousemoveFn = function (ev) { this.mousemoveFn = function (ev) {
if (self._gyroActive) return;
requestAnimationFrame(function () { requestAnimationFrame(function () {
self._layout(ev); var bounds = self.DOM.el.getBoundingClientRect();
self._applyTilt(
ev.clientX - bounds.left,
ev.clientY - bounds.top,
bounds.width,
bounds.height
);
}); });
}; };
this.mouseleaveFn = function (ev) { this.mouseleaveFn = function () {
requestAnimationFrame(function () { requestAnimationFrame(function () {
for (var key in self.DOM.animatable) { self._animateReset();
if (self.options.movement[key] == undefined) {
continue;
}
anime({
targets: self.DOM.animatable[key],
duration:
self.options.movement[key].reverseAnimation != undefined
? self.options.movement[key].reverseAnimation.duration || 0
: 1,
easing:
self.options.movement[key].reverseAnimation != undefined
? self.options.movement[key].reverseAnimation.easing || "linear"
: "linear",
elasticity:
self.options.movement[key].reverseAnimation != undefined
? self.options.movement[key].reverseAnimation.elasticity || null
: null,
scaleX: 1,
scaleY: 1,
scaleZ: 1,
translateX: 0,
translateY: 0,
translateZ: 0,
rotateX: 0,
rotateY: 0,
rotateZ: 0,
});
}
}); });
}; };
this.debounceLeaveFn = function (e) { this.debounceLeaveFn = function (e) {
e.stopPropagation(); e.stopPropagation();
clearTimeout(self.timeout); clearTimeout(self.timeout);
self.timeout = setTimeout(() => { self.timeout = setTimeout(function () {
self.mouseleaveFn(); self._animateReset();
}, 25); }, 25);
}; };
// Touch handlers
this.touchstartFn = function () {
if (self._gyroActive) return;
for (var key in self.DOM.animatable) {
anime.remove(self.DOM.animatable[key]);
}
};
this.touchmoveFn = function (ev) {
ev.preventDefault();
if (self._gyroActive) return;
var touch = ev.touches[0];
if (!touch) return;
requestAnimationFrame(function () {
var bounds = self.DOM.el.getBoundingClientRect();
self._applyTilt(
touch.clientX - bounds.left,
touch.clientY - bounds.top,
bounds.width,
bounds.height
);
});
};
this.DOM.el.addEventListener("mousemove", this.mousemoveFn); this.DOM.el.addEventListener("mousemove", this.mousemoveFn);
this.DOM.el.addEventListener("mouseleave", this.mouseleaveFn); this.DOM.el.addEventListener("mouseleave", this.mouseleaveFn);
this.DOM.el.addEventListener("mouseenter", this.mouseenterFn); this.DOM.el.addEventListener("mouseenter", this.mouseenterFn);
this.DOM.el.addEventListener("touchstart", this.touchstartFn, { passive: true });
this.DOM.el.addEventListener("touchmove", this.touchmoveFn, { passive: false });
this.DOM.el.addEventListener("touchend", this.debounceLeaveFn); this.DOM.el.addEventListener("touchend", this.debounceLeaveFn);
}; };
TiltFx.prototype._layout = function (ev) { /**
// Mouse position relative to the document. * Apply tilt from a relative position within the card.
var mousepos = getMousePos(ev), */
// Document scrolls. TiltFx.prototype._applyTilt = function (relX, relY, width, height) {
docScrolls = {
left: document.body.scrollLeft + document.documentElement.scrollLeft,
top: document.body.scrollTop + document.documentElement.scrollTop,
},
bounds = this.DOM.el.getBoundingClientRect(),
// Mouse position relative to the main element (this.DOM.el).
relmousepos = {
x: mousepos.x - bounds.left - docScrolls.left,
y: mousepos.y - bounds.top - docScrolls.top,
};
// Movement settings for the animatable elements.
for (var key in this.DOM.animatable) { for (var key in this.DOM.animatable) {
if ( if (
this.DOM.animatable[key] == undefined || this.DOM.animatable[key] == undefined ||
@@ -166,13 +140,9 @@
continue; continue;
} }
var t = var t =
this.options.movement[key] != undefined this.options.movement[key].translation || { x: 0, y: 0, z: 0 },
? this.options.movement[key].translation || { x: 0, y: 0, z: 0 }
: { x: 0, y: 0, z: 0 },
r = r =
this.options.movement[key] != undefined this.options.movement[key].rotation || { x: 0, y: 0, z: 0 },
? this.options.movement[key].rotation || { x: 0, y: 0, z: 0 }
: { x: 0, y: 0, z: 0 },
setRange = function (obj) { setRange = function (obj) {
for (var k in obj) { for (var k in obj) {
if (obj[k] == undefined) { if (obj[k] == undefined) {
@@ -188,20 +158,18 @@
var transforms = { var transforms = {
translation: { translation: {
x: ((t.x[1] - t.x[0]) / bounds.width) * relmousepos.x + t.x[0], x: ((t.x[1] - t.x[0]) / width) * relX + t.x[0],
y: ((t.y[1] - t.y[0]) / bounds.height) * relmousepos.y + t.y[0], y: ((t.y[1] - t.y[0]) / height) * relY + t.y[0],
z: ((t.z[1] - t.z[0]) / bounds.height) * relmousepos.y + t.z[0], z: ((t.z[1] - t.z[0]) / height) * relY + t.z[0],
}, },
rotation: { rotation: {
x: ((r.x[1] - r.x[0]) / bounds.height) * relmousepos.y + r.x[0], x: ((r.x[1] - r.x[0]) / height) * relY + r.x[0],
y: ((r.y[1] - r.y[0]) / bounds.width) * relmousepos.x + r.y[0], y: ((r.y[1] - r.y[0]) / width) * relX + r.y[0],
z: ((r.z[1] - r.z[0]) / bounds.width) * relmousepos.x + r.z[0], z: ((r.z[1] - r.z[0]) / width) * relX + r.z[0],
}, },
}; };
this.DOM.animatable[key].style.WebkitTransform = this.DOM.animatable[ this.DOM.animatable[key].style.transform =
key
].style.transform =
"translateX(" + "translateX(" +
transforms.translation.x + transforms.translation.x +
"px) translateY(" + "px) translateY(" +
@@ -218,5 +186,96 @@
} }
}; };
/**
* Animate back to resting position.
*/
TiltFx.prototype._animateReset = function () {
for (var key in this.DOM.animatable) {
if (this.options.movement[key] == undefined) {
continue;
}
anime({
targets: this.DOM.animatable[key],
duration:
this.options.movement[key].reverseAnimation != undefined
? this.options.movement[key].reverseAnimation.duration || 0
: 1,
easing:
this.options.movement[key].reverseAnimation != undefined
? this.options.movement[key].reverseAnimation.easing || "linear"
: "linear",
elasticity:
this.options.movement[key].reverseAnimation != undefined
? this.options.movement[key].reverseAnimation.elasticity || null
: null,
scaleX: 1,
scaleY: 1,
scaleZ: 1,
translateX: 0,
translateY: 0,
translateZ: 0,
rotateX: 0,
rotateY: 0,
rotateZ: 0,
});
}
};
/**
* Init gyroscope / device orientation tilt.
* On iOS 13+ this requires a user gesture to request permission.
*/
TiltFx.prototype._initGyro = function () {
var self = this;
if (!window.DeviceOrientationEvent) return;
// iOS 13+ requires explicit permission request — skip those devices
if (typeof DeviceOrientationEvent.requestPermission === "function") return;
// On Android and other devices, test if events actually fire
var testHandler = function (e) {
if (e.gamma !== null && e.beta !== null) {
self._bindGyro();
}
window.removeEventListener("deviceorientation", testHandler);
};
window.addEventListener("deviceorientation", testHandler);
};
TiltFx.prototype._bindGyro = function () {
var self = this;
this._gyroActive = true;
// Stop any in-progress touch/mouse animations
for (var key in this.DOM.animatable) {
anime.remove(this.DOM.animatable[key]);
}
window.addEventListener("deviceorientation", function (e) {
requestAnimationFrame(function () {
// beta: front-back tilt (-180 to 180), gamma: left-right tilt (-90 to 90)
var beta = e.beta || 0;
var gamma = e.gamma || 0;
// Clamp to reasonable range
beta = Math.max(-30, Math.min(30, beta));
gamma = Math.max(-30, Math.min(30, gamma));
// Normalize to 0-1 range (centered at 0 degrees = 0.5)
var normX = (gamma + 30) / 60;
var normY = (beta + 30) / 60;
var bounds = self.DOM.el.getBoundingClientRect();
self._applyTilt(
normX * bounds.width,
normY * bounds.height,
bounds.width,
bounds.height
);
});
});
};
window.TiltFx = TiltFx; window.TiltFx = TiltFx;
})(window); })(window);