Compare commits

...

14 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
Travis Heinstrom
03117344a5 Update index.html 2023-12-12 20:13:13 -05:00
Travis Heinstrom
87efc8d355 Update index.html 2023-12-12 20:09:51 -05:00
Tim Holman
9aa449c61c More centering 2022-05-28 13:59:04 -04:00
Tim Holman
564580db40 fix page heights 2022-05-28 13:56:33 -04:00
Tim Holman
51afd353df Cleaned up links 2022-05-28 13:27:42 -04:00
Travis Heinstrom
3ea1bc2762 Update index.html 2022-05-10 01:14:58 -04:00
Travis Heinstrom
615342a55b Update index.html 2022-05-10 01:14:01 -04:00
Travis Heinstrom
6c1ea34570 Update index.html 2022-05-10 01:12:15 -04:00
Tim Holman
e86f1afb2f responsive 2021-12-08 19:25:03 -05:00
Tim Holman
d82aeb744b fuckin ds store 2021-12-07 19:13:10 -05:00
4 changed files with 396 additions and 186 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,84 +1,106 @@
* {
box-sizing: border-box;
box-sizing: border-box;
}
html, body {
height: 100%;
height: 100dvh;
min-height: 100%;
min-height: 100dvh;
overflow: hidden;
margin: 0;
padding: 0;
}
body {
background-color: #0e0e0e;
height: 100vh;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
.background {
background-color: #0e0e0e;
background-image: repeating-linear-gradient(135deg, #000, #19191a 1%, transparent 1.5%, transparent 50% );
background-color: #0e0e0e;
background-image: repeating-linear-gradient(
135deg,
#000,
#19191a 1%,
transparent 1.5%,
transparent 50%
);
background-size: 100px 100px;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
animation: fade-in 2500ms ease-out forwards 1s;
top: 0;
left: 0;
width: 100%;
height: 100%;
animation: fade-in 2750ms ease-out forwards;
opacity: 0;
}
.card {
display: block;
position: relative;
width: 420px;
height: 260px;
color: #fff;
flex: none;
perspective: 1000px;
display: block;
position: relative;
max-width: 500px;
width: 85%;
height: 300px;
color: #fff;
flex: none;
perspective: 1000px;
}
.card__figure {
margin: 0;
width: 100%;
height: 100%;
display: block;
position: relative;
background: #111;
border-radius: 2px;
margin: 0;
width: 100%;
height: 100%;
min-height: 100%;
display: block;
position: relative;
background: linear-gradient(145deg, #161616, #0e0e0e);
border-radius: 5px;
}
.card__figure::before {
content: '';
content: "";
position: absolute;
width: calc(100% - 14px);
height: calc(100% - 14px);
top: 6px;
left: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 2px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 3px;
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 {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
border-radius: 3px;
}
.card__deco--shine div {
position: absolute;
width: 200%;
height: 200%;
top: -50%;
left: -50%;
background-image: linear-gradient(45deg, rgba(0, 0, 0, 0.9) 0%, rgba(200, 200, 200, 0.05) 50%, transparent 100%);
position: absolute;
width: 200%;
height: 200%;
top: -50%;
left: -50%;
background-image: linear-gradient(
45deg,
rgba(0, 0, 0, 0.9) 0%,
rgba(200, 200, 200, 0.05) 50%,
transparent 100%
);
}
figcaption {
height: 100%;
padding: 25px;
height: 100%;
padding: 30px;
color: #eee;
font-family: Avenir;
display: flex;
@@ -88,23 +110,79 @@ figcaption {
h1 {
margin: 0;
font-size: 3.3rem;
line-height: 3.3rem;
font-size: 3.6rem;
line-height: 3.6rem;
font-weight: 900;
letter-spacing: -0.02em;
}
p {
margin: 0;
font-weight: 900;
.links {
display: flex;
justify-content: space-between;
font-weight: 900;
align-items: center;
}
a {
color: #ddd;
transition: color 100ms ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
}
a:hover {
color: #eee;
}
svg {
width: 20px;
height: 20px;
border-radius: 3px;
fill: currentColor;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@media screen and (max-width: 430px) {
.card {
width: 85%;
height: auto;
aspect-ratio: 3 / 4;
max-height: 70dvh;
}
.links span {
font-size: 0.85rem;
}
}
@media screen and (max-width: 380px) {
h1 {
font-size: 2.8rem;
line-height: 2.8rem;
}
}
@media screen and (max-width: 330px) {
h1 {
font-size: 2rem;
line-height: 2rem;
}
}
@media screen and (max-width: 250px) {
h1 {
font-size: 1.3rem;
line-height: 1.3rem;
}
}

View File

@@ -2,40 +2,57 @@
<html lang="en">
<head>
<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>
<meta name="description" content="" />
<!-- Needs Favicon -->
<meta name="description" content="The website of Travis Heinström" />
<link rel="stylesheet" type="text/css" href="./css/style.css" />
</head>
<body>
<div class="background"></div>
<main>
<section class="content">
<div class="card">
<figure class="card__figure">
<div class="card__deco card__deco--shine"><div></div></div>
<figcaption>
<h1>Travis <br/> Heinström</h1>
<p>Things Here<br/>Things Here - Things here</p>
</figcaption>
</figure>
</div>
</section>
</main>
<div class="card">
<figure class="card__figure">
<div class="card__deco card__deco--shine"><div></div></div>
<figcaption>
<h1>
Travis <br />
Heinström
</h1>
<div class="links">
<span>travis@heinstrom.com</span>
<a href="https://www.linkedin.com/in/heinstrom" target="_blank" title="Travis on LinkedIn">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 455 455"
xml:space="preserve"
>
<path
style="fill-rule: evenodd; clip-rule: evenodd"
d="M246.4,204.35v-0.665c-0.136,0.223-0.324,0.446-0.442,0.665H246.4z"
></path>
<path
style="fill-rule: evenodd; clip-rule: evenodd"
d="M0,0v455h455V0H0z M141.522,378.002H74.016V174.906h67.506V378.002z M107.769,147.186h-0.446C84.678,147.186,70,131.585,70,112.085c0-19.928,15.107-35.087,38.211-35.087 c23.109,0,37.31,15.159,37.752,35.087C145.963,131.585,131.32,147.186,107.769,147.186z M385,378.002h-67.524V269.345 c0-27.291-9.756-45.92-34.195-45.92c-18.664,0-29.755,12.543-34.641,24.693c-1.776,4.34-2.24,10.373-2.24,16.459v113.426h-67.537 c0,0,0.905-184.043,0-203.096H246.4v28.779c8.973-13.807,24.986-33.547,60.856-33.547c44.437,0,77.744,29.02,77.744,91.398V378.002 z"
></path>
</svg>
</a>
</div>
</figcaption>
</figure>
</div>
<script src="./js/anime.min.js"></script>
<script src="./js/main.js"></script>
<script>
(function() {
new TiltFx(document.querySelector('.card', {}))
})()
(function () {
new TiltFx(document.querySelector(".card"), {});
})();
</script>
<script defer src="https://a.log.rip/script.js" data-website-id="1e9e073c-dd8c-4dfd-b939-5066f4239877"></script>
</body>
</html>

View File

@@ -1,32 +1,15 @@
(function (window) {
"use strict";
;(function(window) {
'use strict';
// Helper vars and functions.
function extend( a, b ) {
for( var key in b ) {
if( b.hasOwnProperty( key ) ) {
function extend(a, b) {
for (var key in b) {
if (b.hasOwnProperty(key)) {
a[key] = b[key];
}
}
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.
*/
@@ -40,103 +23,132 @@
TiltFx.prototype.options = {
movement: {
imgWrapper : {
translation : {x: 0, y: 0, z: 0},
rotation : {x: -5, y: 5, z: 0},
reverseAnimation : {
duration : 1200,
easing : 'easeOutElastic',
elasticity : 600
}
wrapper: {
translation: { x: 0, y: 0, z: 0 },
rotation: { x: -5, y: 5, z: 0 },
reverseAnimation: {
duration: 1200,
easing: "easeOutElastic",
elasticity: 600,
},
},
shine : {
translation : {x: 50, y: 50, z: 0},
reverseAnimation : {
duration : 1200,
easing : 'easeOutElastic',
elasticity: 600
}
}
}
shine: {
translation: { x: 50, y: 50, z: 0 },
reverseAnimation: {
duration: 1200,
easing: "easeOutElastic",
elasticity: 600,
},
},
},
};
/**
* Init.
*/
TiltFx.prototype._init = function() {
TiltFx.prototype._init = function () {
this.DOM.animatable = {};
this.DOM.animatable.imgWrapper = this.DOM.el.querySelector('.card__figure');
this.DOM.animatable.shine = this.DOM.el.querySelector('.card__deco--shine > div');
this.DOM.animatable.wrapper = this.DOM.el.querySelector(".card__figure");
this.DOM.animatable.shine = this.DOM.el.querySelector(
".card__deco--shine > div"
);
this._gyroActive = false;
this._initEvents();
this._initGyro();
};
/**
* Init/Bind events.
*/
TiltFx.prototype._initEvents = function() {
TiltFx.prototype._initEvents = function () {
var self = this;
this.mouseenterFn = function() {
for(var key in self.DOM.animatable) {
this.mouseenterFn = function () {
for (var key in self.DOM.animatable) {
anime.remove(self.DOM.animatable[key]);
}
};
this.mousemoveFn = function(ev) {
requestAnimationFrame(function() { self._layout(ev); });
};
this.mouseleaveFn = function(ev) {
requestAnimationFrame(function() {
for(var key in self.DOM.animatable) {
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.mousemoveFn = function (ev) {
if (self._gyroActive) return;
requestAnimationFrame(function () {
var bounds = self.DOM.el.getBoundingClientRect();
self._applyTilt(
ev.clientX - bounds.left,
ev.clientY - bounds.top,
bounds.width,
bounds.height
);
});
};
this.DOM.el.addEventListener('mousemove', this.mousemoveFn);
this.DOM.el.addEventListener('mouseleave', this.mouseleaveFn);
this.DOM.el.addEventListener('mouseenter', this.mouseenterFn);
this.mouseleaveFn = function () {
requestAnimationFrame(function () {
self._animateReset();
});
};
this.debounceLeaveFn = function (e) {
e.stopPropagation();
clearTimeout(self.timeout);
self.timeout = setTimeout(function () {
self._animateReset();
}, 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("mouseleave", this.mouseleaveFn);
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);
};
TiltFx.prototype._layout = function(ev) {
// Mouse position relative to the document.
var mousepos = getMousePos(ev),
// Document scrolls.
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) {
if( this.DOM.animatable[key] == undefined || this.options.movement[key] == undefined ) {
/**
* Apply tilt from a relative position within the card.
*/
TiltFx.prototype._applyTilt = function (relX, relY, width, height) {
for (var key in this.DOM.animatable) {
if (
this.DOM.animatable[key] == undefined ||
this.options.movement[key] == undefined
) {
continue;
}
var t = this.options.movement[key] != undefined ? this.options.movement[key].translation || {x:0,y:0,z:0} : {x:0,y:0,z:0},
r = this.options.movement[key] != undefined ? this.options.movement[key].rotation || {x:0,y:0,z:0} : {x:0,y:0,z:0},
var t =
this.options.movement[key].translation || { x: 0, y: 0, z: 0 },
r =
this.options.movement[key].rotation || { x: 0, y: 0, z: 0 },
setRange = function (obj) {
for(var k in obj) {
if( obj[k] == undefined ) {
obj[k] = [0,0];
}
else if( typeof obj[k] === 'number' ) {
obj[k] = [-1*obj[k],obj[k]];
for (var k in obj) {
if (obj[k] == undefined) {
obj[k] = [0, 0];
} else if (typeof obj[k] === "number") {
obj[k] = [-1 * obj[k], obj[k]];
}
}
};
@@ -145,22 +157,125 @@
setRange(r);
var transforms = {
translation : {
x: (t.x[1]-t.x[0])/bounds.width*relmousepos.x + t.x[0],
y: (t.y[1]-t.y[0])/bounds.height*relmousepos.y + t.y[0],
z: (t.z[1]-t.z[0])/bounds.height*relmousepos.y + t.z[0],
translation: {
x: ((t.x[1] - t.x[0]) / width) * relX + t.x[0],
y: ((t.y[1] - t.y[0]) / height) * relY + t.y[0],
z: ((t.z[1] - t.z[0]) / height) * relY + t.z[0],
},
rotation: {
x: ((r.x[1] - r.x[0]) / height) * relY + r.x[0],
y: ((r.y[1] - r.y[0]) / width) * relX + r.y[0],
z: ((r.z[1] - r.z[0]) / width) * relX + r.z[0],
},
rotation : {
x: (r.x[1]-r.x[0])/bounds.height*relmousepos.y + r.x[0],
y: (r.y[1]-r.y[0])/bounds.width*relmousepos.x + r.y[0],
z: (r.z[1]-r.z[0])/bounds.width*relmousepos.x + r.z[0]
}
};
this.DOM.animatable[key].style.WebkitTransform = this.DOM.animatable[key].style.transform = 'translateX(' + transforms.translation.x + 'px) translateY(' + transforms.translation.y + 'px) translateZ(' + transforms.translation.z + 'px) rotateX(' + transforms.rotation.x + 'deg) rotateY(' + transforms.rotation.y + 'deg) rotateZ(' + transforms.rotation.z + 'deg)';
this.DOM.animatable[key].style.transform =
"translateX(" +
transforms.translation.x +
"px) translateY(" +
transforms.translation.y +
"px) translateZ(" +
transforms.translation.z +
"px) rotateX(" +
transforms.rotation.x +
"deg) rotateY(" +
transforms.rotation.y +
"deg) rotateZ(" +
transforms.rotation.z +
"deg)";
}
};
window.TiltFx = TiltFx;
/**
* 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);