Найден корректный robots.txt
Файл управления сканированием сайта роботами
?
Robots.txt указывает поисковым роботам, какие страницы сканировать, а какие — нет. Ошибки в файле могут случайно закрыть важные разделы от индексации.
Robots.txt настроен корректно. Размер файла: 34747 байт. Загружен за: 0сек.
Проверяемая страница не запрещена в robots.txt.
Показать содержимое robots.txt
<!DOCTYPE html><html lang="en" data-astro-cid-j7pv25f6> <head><!-- Global Metadata --><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="sitemap" href="/sitemap-index.xml"><link rel="alternate" type="application/rss+xml" title="Ace of Cups - Luke Murphy's personal website" href="https://lurkmoophy.com/rss.xml"><meta name="generator" content="Astro v5.14.5"><!-- Font preloads --><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,300;0,400;0,900;1,300;1,400;1,900&family=Alegreya:ital,wght@0,400..900;1,400..900&family=Allura&family=Gideon+Roman&family=Rakkas&family=Hanken+Grotesk:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet"><!-- GSAP Library - Client-side only --><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><script src="https://cdn.jsdelivr.net/npm/gsap@3.12.2/dist/ScrambleTextPlugin.min.js"></script><!-- Canonical URL --><link rel="canonical" href="https://lurkmoophy.com/"><!-- Primary Meta Tags --><title>Ace of Cups - Luke Murphy's personal website</title><meta name="title" content="Ace of Cups - Luke Murphy's personal website"><meta name="description" content="This is the personal site (and portfolio) of Luke Murphy, the Australian-cum-British designer, developer, writer, speaker and general reprobate."><!-- Open Graph / Facebook --><meta property="og:type" content="website"><meta property="og:url" content="https://lurkmoophy.com/"><meta property="og:title" content="Ace of Cups - Luke Murphy's personal website"><meta property="og:description" content="This is the personal site (and portfolio) of Luke Murphy, the Australian-cum-British designer, developer, writer, speaker and general reprobate."><!-- Twitter --><meta property="twitter:card" content="summary"><meta property="twitter:url" content="https://lurkmoophy.com/"><meta property="twitter:title" content="Ace of Cups - Luke Murphy's personal website"><meta property="twitter:description" content="This is the personal site (and portfolio) of Luke Murphy, the Australian-cum-British designer, developer, writer, speaker and general reprobate."><link rel="stylesheet" href="/_astro/_slug_.Bt8IZND1.css">
<link rel="stylesheet" href="/_astro/index.CjDqb5pH.css">
<style>.title{border-top:7px solid var(--black);border-bottom:7px solid var(--black);font-family:Gideon Roman,serif;padding:.15em 0 .15em 3vw;margin:0 0 .5em;font-size:clamp(3rem,.75rem + 10vw,5rem);font-weight:400;text-transform:uppercase;text-align:left;overflow:hidden;text-wrap:nowrap;filter:blur(.7px)}.title-track{display:block;will-change:transform}.title-marquee{color:var(--grey);opacity:.2}.description{font-size:1.25rem;font-style:italic;margin-bottom:1em;text-transform:none;font-style:normal;font-family:Alegreya Sans,sans-serif;padding-left:5%}
</style></head> <body data-astro-cid-j7pv25f6> <div class="static-bg" data-astro-cid-3ef6ksr2></div> <header data-astro-cid-3ef6ksr2> <nav data-astro-cid-3ef6ksr2> <a href="/" class="active" data-astro-cid-3ef6ksr2="true" data-astro-cid-eimmu3lg> Home </a> <a href="/blog" data-astro-cid-3ef6ksr2="true" data-astro-cid-eimmu3lg> Writing </a> <a href="/product-design" data-astro-cid-3ef6ksr2="true" data-astro-cid-eimmu3lg> Product Design </a> <a href="/side-projects" data-astro-cid-3ef6ksr2="true" data-astro-cid-eimmu3lg> Side Projects </a> <a href="/illustration" data-astro-cid-3ef6ksr2="true" data-astro-cid-eimmu3lg> Illustration </a> <a href="/speaking" data-astro-cid-3ef6ksr2="true" data-astro-cid-eimmu3lg> Speaking </a> </nav> </header> <script type="module">const e=document.getElementById("marquee-container"),t=document.getElementById("marquee");e&&t&&(e.addEventListener("mouseenter",()=>{t.style.setProperty("--play-state","paused")}),e.addEventListener("mouseleave",()=>{t.style.setProperty("--play-state","running")}));</script> <main data-astro-cid-j7pv25f6> <section class="index--masthead" data-astro-cid-j7pv25f6> <svg style="visibility: hidden; position: absolute;" width="0" height="0" xmlns="http://www.w3.org/2000/svg" data-astro-cid-j7pv25f6> <defs data-astro-cid-j7pv25f6> <filter id="round-corners" data-astro-cid-j7pv25f6> <feGaussianBlur in="SourceGraphic" stdDeviation="2" result="blur" data-astro-cid-j7pv25f6></feGaussianBlur> <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 25 -9" result="goo" data-astro-cid-j7pv25f6></feColorMatrix> <feComposite in="SourceGraphic" in2="goo" operator="atop" data-astro-cid-j7pv25f6></feComposite> </filter> </defs> </svg> <div class="index--masthead--background riso" data-astro-cid-j7pv25f6></div> <div class="index--masthead--clouds--back riso" data-astro-cid-j7pv25f6></div> <div class="index--masthead--clouds--front riso" data-astro-cid-j7pv25f6></div> <h2 class="index--masthead--subhead" data-astro-cid-j7pv25f6>The Personal Homepage of Luke Murphy</h2> <h1 class="index--masthead--head" data-astro-cid-j7pv25f6><span class="riso" data-astro-cid-j7pv25f6>Still Here</span></h1> <div class="unicorn" data-astro-cid-j7pv25f6></div> </section> <section class="index--intro" data-astro-cid-j7pv25f6> <div class="intro-big" data-astro-cid-j7pv25f6>Hi, I'm Luke.</div> <div class="intro-text" data-astro-cid-j7pv25f6> <p data-astro-cid-j7pv25f6>I'm a designer, tinkerer, writer, and speaker. I currently work at <a href="https://www.zeroheight.com" data-astro-cid-j7pv25f6>zeroheight</a> as a Principal Design Advocate, and used to work at <a href="https://www.memrise.com" data-astro-cid-j7pv25f6>Memrise</a>, <a href="https://www.microsoft.com" data-astro-cid-j7pv25f6>Microsoft</a>, and <a href="https://www.mrandmrsmith.com" data-astro-cid-j7pv25f6>Mr & Mrs Smith</a> (amongst others). I also run <a href="https://nothingsoundsgood.club" data-astro-cid-j7pv25f6>Nothing Sounds Good</a>, co-host the <a href="https://open.spotify.com/show/0B7uAAhjEGQi66Fo3Bd8Dx?si=302749426dbe45e0" data-astro-cid-j7pv25f6>Design Systems WTF podcast</a>, and generally do too many side projects.</p> <p data-astro-cid-j7pv25f6>I am currently building this site as I go, so it might be buggy af. Want to follow the progress? Follow me on <a href="https://bsky.app/profile/lurkmoophy.bsky.social" data-astro-cid-j7pv25f6>Bluesky</a>.</p> </div> <div class="index--face" role="img" aria-label="Luke Murphy looking around" data-astro-cid-j7pv25f6></div> </section> <section class="index--proud" data-astro-cid-j7pv25f6> <h3 class="title" id="title-things-i'm-proud-of"> <span class="title-track"> <span class="title-marquee" aria-hidden="true"><Things I'm Proud Of></span> <span class="title-text"><Things I'm Proud Of></span> <span class="title-marquee" aria-hidden="true"><Things I'm Proud Of> <Things I'm Proud Of> <Things I'm Proud Of> </span> </span> </h3> <script type="module">document.addEventListener("DOMContentLoaded",()=>{const f=document.querySelectorAll(".title"),a=window.matchMedia("(prefers-reduced-motion: reduce)").matches,g=80,i=20,h=.045,p=.1,l=[...f].flatMap(t=>{const e=t.querySelector(".title-track");return e instanceof HTMLElement?[{titleEl:t,track:e,current:0,target:0,ready:!1}]:[]});function E(t){const e=t.querySelector(".title-text");return e instanceof HTMLElement?e.getBoundingClientRect().width+i:i}function w(t){const e=t.getBoundingClientRect(),c=window.innerHeight,r=e.top,d=c,o=0;let n=0;return r<d&&r>o?n=1-(r-o)/(d-o):r<=o&&(n=1),n=Math.max(0,Math.min(1,n)),1-Math.pow(1-n,3)}function s(){l.forEach(t=>{const e=E(t.titleEl),c=a?0:g*w(t.titleEl);t.target=-e+c,t.ready||(t.current=t.target,t.ready=!0,t.track.style.transform=`translateX(${t.current}px)`)})}function u(){l.forEach(t=>{const e=t.target-t.current;Math.abs(e)>p?t.current+=e*h:t.current=t.target,t.track.style.transform=`translateX(${t.current}px)`}),requestAnimationFrame(u)}s(),window.addEventListener("scroll",s,{passive:!0}),window.addEventListener("resize",s),a||requestAnimationFrame(u)});</script> <div class="side__project-container" data-astro-cid-j7pv25f6> <div class="side__project-filler filler-left" data-astro-cid-j7pv25f6></div> <div class="side__project-individual grid-nudge-right"> <div class="side__project-individual-image"> <img src="/images/logo-baseline.svg" alt="" aria-hidden="true" data-astro-cid-j7pv25f6> </div> <div class="side__project-individual-title-background"> <h2>Baseline Design System</h2> </div> <div class="side__project-individual-content"> <h2 class="side__project-individual-title">Baseline Design System</h2> <p class="side__project-individual-description">An enterprise-level design system and suite of Figma plugins, built from scratch for zeroheight: multi-brand tokens, a React library, Storybook, and docs that could ship a real product.</p> <div class="side__project-individual-link"> <a href="/product-design/baseline-design-system/"> Read the case study </a> </div> </div> </div> <div class="side__project-individual grid-nudge-left"> <div class="side__project-individual-image"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.82 123.74" aria-hidden="true" data-astro-cid-j7pv25f6> <path d="M5.56,65.91c.88-5.71.87-11.85,1.84-17.67,1.92-11.46,7.6-23.94,16.62-31.41l2.48,1.36,2.01,19.44c7.49,2.23,15.46,2.42,23.22,1.69,10.73-1.01,20.34-2.65,31.34-3.02,3.76-.12,7.58.36,11.31-.27l.52-.46c1.31-3.19,4.1-15.76,6.88-17.05,1.4-.65,2.1-.21,2.93.92,3.79,5.18,7.45,16.94,8.88,23.3,3.85,17.18,4.76,35.07,2.35,52.53l-2.8,2.35c-14.29-.3-28.66.48-42.95.04-6.27-.19-13.06-1.28-19.29-1.32-11.69-.09-23.46,1.98-35.12-.1-5.32-.95-6.26-2.2-7.85-7.18-.99-3.11-2.11-8.37-2.36-11.58v-11.58ZM38.48,74.76c1.66-1.66,4.12-7.22,4.41-9.53.54-4.27-4.42-9.73-8.76-9.65-7.7.15-12.37,6.71-12.28,13.97.11,8.59,11.24,10.6,16.63,5.21ZM88.36,57.45c-7.26.91-8.39,7.05-5.99,13.04,1.62,4.05,3.04,7.46,7.98,6.96,4.3-.44,8.91-1.94,10.35-6.39,2.84-8.8-3.82-14.66-12.33-13.6ZM53.37,69.91c-.25.23-.85,1.19-.91,1.48-.47,2.48,5.43,8.34,5.89,11.5.24,1.64-.29,3.43.55,5.03l.92.68c2.57-.81,1.7-2.91,2.1-4.79.92-4.3,10.69-8.41,8.86-13.77-2.82-3.1-6.79.42-9.21.58-2.1.14-5.53-3.16-8.21-.72ZM32.19,82.64c-.27,2.66-3.89,6.43-1.09,8.58l2.1.75,3.25-2.65c.28-1.31-3.08-7.54-4.26-6.68Z" data-astro-cid-j7pv25f6></path> <path d="M32.09,62.17c1.49-.17,3.09-.43,4.38.53,2.57,1.85.49,4.84-1.49,6.21-7.27,5.05-13.24-5.57-2.89-6.74Z" data-astro-cid-j7pv25f6></path> <path d="M89.63,62.57c3.41-.69,6.77,1.62,7.2,5.07-2.96,3.99-8.87,4.35-11.29-.33-.41-1.95,2.34-4.38,4.1-4.73Z" data-astro-cid-j7pv25f6></path> </svg> </div> <div class="side__project-individual-title-background"> <h2>Nothing Sounds Good</h2> </div> <div class="side__project-individual-content"> <h2 class="side__project-individual-title">Nothing Sounds Good</h2> <p class="side__project-individual-description">A vinyl subscription box sending DIY indie, punk and emo records to UK subscribers every month. I'm Creative Director and Head of Product. My second vinyl subscription after Wax & Stamp.</p> <div class="side__project-individual-link"> <a href="https://nothingsoundsgood.club" target="_blank" rel="noopener noreferrer"> Check it out </a> </div> </div> </div> <div class="side__project-filler filler-right" data-astro-cid-j7pv25f6></div> <div class="side__project-individual grid-half"> <div class="side__project-individual-image"> <img src="/images/logo-talk.svg" alt="" aria-hidden="true" data-astro-cid-j7pv25f6> </div> <div class="side__project-individual-title-background"> <h2>Hatch 2023</h2> </div> <div class="side__project-individual-content"> <h2 class="side__project-individual-title">Hatch 2023</h2> <p class="side__project-individual-description">My Hatch 2023 talk on how design systems can encode exclusion, and what we can do to stop reinforcing systems of harm by accident.</p> <div class="side__project-individual-link"> <a href="https://www.youtube.com/watch?v=iLtVIEcHdSo" target="_blank" rel="noopener noreferrer"> Watch the talk </a> </div> </div> </div> <div class="side__project-individual grid-half"> <div class="side__project-individual-image"> <img src="/images/logo-maturity.svg" alt="" aria-hidden="true" data-astro-cid-j7pv25f6> </div> <div class="side__project-individual-title-background"> <h2>Maturity</h2> </div> <div class="side__project-individual-content"> <h2 class="side__project-individual-title">Design System Maturity Model</h2> <p class="side__project-individual-description">A new way to think about design system maturity, written for zeroheight. Practical framing for teams figuring out where they are and what to do next.</p> <div class="side__project-individual-link"> <a href="https://zeroheight.com/blog/a-new-design-system-maturity-model/" target="_blank" rel="noopener noreferrer"> Read the post </a> </div> </div> </div> <div class="side__project-individual grid-nudge-left-more"> <div class="side__project-individual-image"> <img src="/images/logo-lc.svg" alt="" aria-hidden="true" data-astro-cid-j7pv25f6> </div> <div class="side__project-individual-title-background"> <h2>Los Campesinos!</h2> </div> <div class="side__project-individual-content"> <h2 class="side__project-individual-title">Los Campesinos! Football References</h2> <p class="side__project-individual-description">A mini digital zine cataloguing every football reference buried in the Los Campesinos! catalogue. Originally written for the Nothing Sounds Good zine, then expanded into a full interactive piece.</p> <div class="side__project-individual-link"> <a href="/blog/los-campesinos-football-references/"> Read the zine </a> </div> </div> </div> <div class="side__project-filler filler-right-more" data-astro-cid-j7pv25f6></div> </div> </section> <section class="index--section" data-astro-cid-j7pv25f6> <h3 class="title" id="title-manifesto"> <span class="title-track"> <span class="title-marquee" aria-hidden="true"><Manifesto></span> <span class="title-text"><Manifesto></span> <span class="title-marquee" aria-hidden="true"><Manifesto> <Manifesto> <Manifesto> </span> </span> </h3> <div class="manifesto-container" data-astro-cid-j7pv25f6> <div class="text-container" data-astro-cid-j7pv25f6> <p class="dropcap" data-astro-cid-j7pv25f6>
Why start a page with a boring about section? Well, this is still a boring about section, but restyled in the form of a manifesto. You possibly want to know a bit more about me, but I also think it’s important to share the things you indelibly believe in at the same time.
</p> <p data-astro-cid-j7pv25f6>
So, let’s start with the basic stuff to get it out of the way. I’m a 40-something person who has spent most of their career in some mixture of product design, front-end development, writing and speaking, and carrying on too many side projects to feasibly keep going. My pronouns are they/them (using the correct pronouns makes you a cooler person. Fact), and I currently call Letchworth Garden City, UK my home.
</p> <p data-astro-cid-j7pv25f6>
I believe in making things for the internet that don’t suck, both from a ‘putting joy into the world’ point of view, as well as a ‘not making things shitty for people by not thinking about accessibility and inclusivity’.
</p> <p data-astro-cid-j7pv25f6>
I believe that trans rights are human rights, that borders shouldn’t exist, that diversity and equity should be front and centre of most things, and that Josie and the Pussycats is the greatest film of all-time.
</p> </div> <div class="manifesto-statements" data-astro-cid-j7pv25f6> <div class="statement" data-astro-cid-j7pv25f6> <div class="statement-text" data-text="GENDER CRITICAL VIEWS ARE BIGOTRY" data-astro-cid-j7pv25f6>GENDER CRITICAL VIEWS ARE BIGOTRY</div> </div> <div class="statement" data-astro-cid-j7pv25f6> <div class="statement-text" data-text="BORDERS ARE BULLSHIT" data-astro-cid-j7pv25f6>BORDERS ARE BULLSHIT</div> </div> <div class="statement" data-astro-cid-j7pv25f6> <div class="statement-text" data-text="REFORM UK ARE FASCISTS" data-astro-cid-j7pv25f6>REFORM UK ARE FASCISTS</div> </div> <div class="statement" data-astro-cid-j7pv25f6> <div class="statement-text" data-text="DEI IS ESSENTIAL" data-astro-cid-j7pv25f6>DEI IS ESSENTIAL</div> </div> <div class="statement" data-astro-cid-j7pv25f6> <div class="statement-text" data-text="JOSIE AND THE PUSSYCATS 4LYF" data-astro-cid-j7pv25f6>JOSIE AND THE PUSSYCATS 4LYF</div> </div> </div> </div> </section> </main> <script>
// Face sprite: eyes follow cursor, blink every 5s
// Frame order: top, right, down, left, center, eyes-closed
document.addEventListener('DOMContentLoaded', () => {
const face = document.querySelector('.index--face');
if (!face) return;
const FRAMES = { top: 0, right: 1, down: 2, left: 3, center: 4, blink: 5 };
const BLINK_INTERVAL_MS = 5000;
const BLINK_DURATION_MS = 100;
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let lookFrame = FRAMES.center;
let isBlinking = false;
let lastPointer = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
function setFrame(frame) {
const width = face.offsetWidth;
face.style.backgroundPosition = `${-(frame * width)}px 0`;
}
function getLookFrame(clientX, clientY) {
const rect = face.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dx = clientX - cx;
const dy = clientY - cy;
const distance = Math.hypot(dx, dy);
const deadZone = Math.max(rect.width, rect.height) * 0.55;
if (distance < deadZone) return FRAMES.center;
const deg = Math.atan2(dy, dx) * (180 / Math.PI);
if (deg >= -45 && deg < 45) return FRAMES.right;
if (deg >= 45 && deg < 135) return FRAMES.down;
if (deg >= -135 && deg < -45) return FRAMES.top;
return FRAMES.left;
}
function updateLook() {
if (isBlinking || prefersReducedMotion) return;
lookFrame = getLookFrame(lastPointer.x, lastPointer.y);
setFrame(lookFrame);
}
function blink() {
if (isBlinking) return;
isBlinking = true;
setFrame(FRAMES.blink);
setTimeout(() => {
isBlinking = false;
updateLook();
}, BLINK_DURATION_MS);
}
setFrame(FRAMES.center);
if (prefersReducedMotion) return;
window.addEventListener('pointermove', (e) => {
lastPointer = { x: e.clientX, y: e.clientY };
updateLook();
});
window.addEventListener('resize', () => {
if (isBlinking) {
setFrame(FRAMES.blink);
} else {
setFrame(lookFrame);
}
});
face.addEventListener('click', blink);
setInterval(blink, BLINK_INTERVAL_MS);
});
</script> <script>
// Unicorn: scroll-driven move + walk cycle, with ease-out that trails past scroll stop
document.addEventListener('DOMContentLoaded', () => {
const unicorn = document.querySelector('.unicorn');
if (!unicorn) return;
const totalFrames = 5;
const speedMultiplier = 7;
const EASE = 0.065;
const SNAP_PX = 0.1;
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let targetLeft = 0;
let currentLeft = 0;
let targetFrameProgress = 0;
let currentFrameProgress = 0;
let ready = false;
function isMobile() {
return window.getComputedStyle(unicorn).position === 'relative';
}
function updateTargets() {
const scrollTop = window.pageYOffset;
const documentHeight = Math.max(
document.documentElement.scrollHeight - window.innerHeight,
1
);
const unicornWidth = unicorn.getBoundingClientRect().width;
targetFrameProgress = ((scrollTop * speedMultiplier) / documentHeight) * totalFrames;
if (!isMobile()) {
const scrollPercent = Math.min(scrollTop / documentHeight, 1);
const offset = unicornWidth - 20;
const startPosition = window.innerWidth - offset;
const endPosition = -offset;
targetLeft = startPosition - scrollPercent * (startPosition - endPosition);
}
if (!ready) {
currentLeft = targetLeft;
currentFrameProgress = targetFrameProgress;
ready = true;
apply();
}
}
function apply() {
const unicornWidth = unicorn.getBoundingClientRect().width;
const frame = ((Math.floor(currentFrameProgress) % totalFrames) + totalFrames) % totalFrames;
unicorn.style.backgroundPosition = `${-(frame * unicornWidth)}px 0`;
if (isMobile()) {
unicorn.style.left = '';
} else {
unicorn.style.left = `${currentLeft}px`;
}
}
function tick() {
const frameDelta = targetFrameProgress - currentFrameProgress;
if (Math.abs(frameDelta) > 0.001) {
currentFrameProgress += frameDelta * EASE;
} else {
currentFrameProgress = targetFrameProgress;
}
if (!isMobile()) {
const leftDelta = targetLeft - currentLeft;
if (Math.abs(leftDelta) > SNAP_PX) {
currentLeft += leftDelta * EASE;
} else {
currentLeft = targetLeft;
}
}
apply();
requestAnimationFrame(tick);
}
updateTargets();
window.addEventListener('scroll', updateTargets, { passive: true });
window.addEventListener('resize', updateTargets);
if (prefersReducedMotion) {
window.addEventListener('scroll', () => {
currentLeft = targetLeft;
currentFrameProgress = targetFrameProgress;
apply();
}, { passive: true });
} else {
requestAnimationFrame(tick);
}
});
</script> <script>
// Scramble text in elements with .scramble class
(function() {
// Wait for GSAP and ScrambleTextPlugin to be available
function checkAndInit() {
if (typeof window === 'undefined') return;
if (typeof gsap === 'undefined') {
setTimeout(checkAndInit, 100);
return;
}
// Register GSAP plugins
if (typeof ScrollTrigger !== 'undefined') {
gsap.registerPlugin(ScrollTrigger);
}
if (typeof ScrambleTextPlugin !== 'undefined') {
gsap.registerPlugin(ScrambleTextPlugin);
}
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initScramble);
} else {
initScramble();
}
}
function createScrambleAnimation(element, textToScramble, scrambleInner, htmlToScramble) {
// Try to use ScrambleTextPlugin if available, otherwise use custom implementation
if (typeof ScrambleTextPlugin !== 'undefined') {
// Use official ScrambleTextPlugin with ScrollTrigger
gsap.to(scrambleInner, {
duration: 2,
scrambleText: {
text: textToScramble,
chars: "fwqhiooiwfqhiofwqokljlsdakml",
revealDelay: 0.5,
tweenLength: false
},
scrollTrigger: {
trigger: element,
start: "top 80%",
toggleActions: "play none none none",
once: true
},
onComplete: function() {
// Restore HTML if it was provided
if (htmlToScramble) {
scrambleInner.innerHTML = htmlToScramble;
}
}
});
} else {
// Fallback: Custom scramble effect using GSAP timeline
// Use uppercase letters that match the original text width
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
function getRandomChar() {
return chars[Math.floor(Math.random() * chars.length)];
}
function scrambleText(original, progress) {
let result = '';
const revealLength = Math.floor(original.length * progress);
for (let i = 0; i < original.length; i++) {
if (original[i] === ' ') {
result += ' ';
} else if (i < revealLength) {
// Revealed character
result += original[i];
} else {
// Scrambled character
result += getRandomChar();
}
}
return result;
}
// Create timeline for scramble effect (no repeat)
const tl = gsap.timeline({
scrollTrigger: {
trigger: element,
start: "top 80%",
toggleActions: "play none none none",
once: true
}
});
// Scramble phase
tl.to({ progress: 0 }, {
duration: 0.2,
progress: 1,
onUpdate: function() {
scrambleInner.textContent = scrambleText(textToScramble, 0);
},
ease: 'none'
});
// Reveal phase
tl.to({ progress: 0 }, {
duration: 0.7,
progress: 1,
onUpdate: function() {
const p = this.progress();
scrambleInner.textContent = scrambleText(textToScramble, p);
},
ease: 'power2.out'
});
// Hold at full text, then restore HTML if provided
if (htmlToScramble) {
tl.set(scrambleInner, { innerHTML: htmlToScramble });
} else {
tl.set(scrambleInner, { textContent: textToScramble });
}
}
}
function initScramble() {
const scrambleElements = document.querySelectorAll('.scramble');
if (scrambleElements.length === 0) {
console.warn('No elements with .scramble class found');
return;
}
// Now process scramble elements
scrambleElements.forEach((element) => {
const originalHTML = element.innerHTML;
// Extract content between < and >, preserving HTML
// Match both <...> (encoded) and <...> (decoded) formats
// Use a more specific pattern to handle nested HTML tags
const match = originalHTML.match(/^(?:<|<)(.+?)(?:>|>)$/) || originalHTML.match(/(?:<|<)(.+?)(?:>|>)/);
if (match) {
// Element has text in <text> format
const htmlToScramble = match[1]; // The HTML content between < and >
// Get plain text version for scrambling animation
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlToScramble;
const textToScramble = tempDiv.textContent || tempDiv.innerText || '';
// Create new structure with brackets and scramble span containing the HTML
element.innerHTML = '<<span class="scramble-inner">' + htmlToScramble + '</span>>';
const scrambleInner = element.querySelector('.scramble-inner');
if (!scrambleInner) {
console.error('Failed to create scramble inner element');
return;
}
createScrambleAnimation(element, textToScramble, scrambleInner, htmlToScramble);
} else {
// Element is a container - scramble all direct child text elements
// Skip statement-text elements (no scramble for manifesto statements)
const childTextElements = element.querySelectorAll('[class*="text"]:not(.statement-text), p, h1, h2, h3, h4, h5, h6');
if (childTextElements.length === 0) {
// If no specific children found, try to scramble the element's own text
const textContent = element.textContent.trim();
if (textContent) {
// Store original width to prevent layout shifts
const originalWidth = element.getBoundingClientRect().width;
if (originalWidth > 0 && element instanceof HTMLElement) {
element.style.width = `${originalWidth}px`;
element.style.minWidth = `${originalWidth}px`;
}
const scrambleInner = document.createElement('span');
scrambleInner.className = 'scramble-inner';
scrambleInner.textContent = textContent;
element.innerHTML = '';
element.appendChild(scrambleInner);
createScrambleAnimation(element, textContent, scrambleInner);
}
return;
}
// Set up scramble animation for each element
childTextElements.forEach((childElement) => {
const textContent = childElement.textContent.trim();
if (!textContent) return;
if (!(childElement instanceof HTMLElement)) return;
// Create the scramble inner element
const scrambleInner = document.createElement('span');
scrambleInner.className = 'scramble-inner';
scrambleInner.textContent = textContent;
childElement.innerHTML = '';
childElement.appendChild(scrambleInner);
createScrambleAnimation(childElement, textContent, scrambleInner);
});
}
});
}
// Start checking once scripts are loaded
checkAndInit();
})();
</script> <script>
// Animate displacement filter scale: flickering at top, exponential scroll-based below
document.addEventListener('DOMContentLoaded', () => {
const displacementMap = document.getElementById('displacement');
if (!displacementMap) return;
const minScale = 10;
const maxScale = 500;
const exponent = 0.7; // Exponential curve (higher = more exponential growth)
const flickerMin = 0;
const flickerMax = 10;
const flickerSpeed = 50; // Milliseconds between flicker updates during flicker phase
const minFlickerDuration = 200; // Minimum milliseconds to flicker
const maxFlickerDuration = 800; // Maximum milliseconds to flicker
const minStillDuration = 500; // Minimum milliseconds to be still
const maxStillDuration = 2000; // Maximum milliseconds to be still
let flickerAnimationId = null;
let isFlickering = false;
let isFlickerPhase = false; // true = flickering, false = still
let flickerStartTime = 0;
let flickerDuration = 0;
function startFlickering() {
if (isFlickering) return;
isFlickering = true;
isFlickerPhase = true; // Start with flickering
flickerStartTime = 0; // Reset timing
function flickerLoop() {
if (!isFlickering) return;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
// Only flicker if we're at the very top
if (scrollTop !== 0) {
isFlickering = false;
return;
}
if (isFlickerPhase) {
// Flickering phase: rapidly change values
const randomScale = flickerMin + Math.random() * (flickerMax - flickerMin);
displacementMap.setAttribute('scale', randomScale.toFixed(2));
// Check if we need to start a new flicker phase
const now = Date.now();
if (flickerStartTime === 0) {
// Start new flicker phase
flickerStartTime = now;
flickerDuration = minFlickerDuration + Math.random() * (maxFlickerDuration - minFlickerDuration);
}
// Check if flicker phase should end
if (now - flickerStartTime >= flickerDuration) {
// Switch to still phase
isFlickerPhase = false;
flickerStartTime = 0;
displacementMap.setAttribute('scale', flickerMin.toFixed(2));
// Stay still for a random duration
const stillDuration = minStillDuration + Math.random() * (maxStillDuration - minStillDuration);
flickerAnimationId = setTimeout(() => {
// Switch back to flickering phase
isFlickerPhase = true;
flickerStartTime = 0;
flickerLoop();
}, stillDuration);
} else {
// Continue flickering
flickerAnimationId = setTimeout(flickerLoop, flickerSpeed);
}
}
}
flickerLoop();
}
function stopFlickering() {
isFlickering = false;
if (flickerAnimationId) {
clearTimeout(flickerAnimationId);
flickerAnimationId = null;
}
}
function updateScale() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const documentHeight = document.documentElement.scrollHeight - window.innerHeight;
// Calculate scroll progress (0 to 1)
const scrollProgress = Math.min(Math.max(scrollTop / documentHeight, 0), 1);
// If at the top, use flickering effect
if (scrollTop === 0) {
startFlickering();
return; // Flickering function will handle the update
} else {
stopFlickering();
// Apply exponential curve: scale increases exponentially as you scroll down
const exponentialProgress = Math.pow(scrollProgress, exponent);
const scale = minScale + (maxScale - minScale) * exponentialProgress;
// Update the scale attribute
displacementMap.setAttribute('scale', scale.toFixed(2));
}
}
window.addEventListener('scroll', updateScale);
updateScale(); // Initial call
});
</script> <section class="footer" data-astro-cid-sz7xmlte> <img src="/images/footer-top.svg" class="footer-top" data-astro-cid-sz7xmlte> <footer data-astro-cid-sz7xmlte> <div class="footer-content" data-astro-cid-sz7xmlte> <p class="footer-content--version" data-astro-cid-sz7xmlte>† v4.1</p> <p class="last-updated" data-astro-cid-sz7xmlte>
Last updated: Aug 13, 2026 </p> </div> <div class="footer-content" data-astro-cid-sz7xmlte> <p class="footer-content--header" data-astro-cid-sz7xmlte>Shit I do</p> <a href="/blog" data-astro-cid-sz7xmlte>Writing</a> <a href="/product-design" data-astro-cid-sz7xmlte>Product Design</a> <a href="/side-projects" data-astro-cid-sz7xmlte>Side Projects</a> <a href="/illustration" data-astro-cid-sz7xmlte>Illustration</a> <a href="/speaking" data-astro-cid-sz7xmlte>Speaking</a> <p class="footer-content--header" data-astro-cid-sz7xmlte>Other shit</p> <a class="coming-soon" data-astro-cid-sz7xmlte>About (coming soon)</a> <a href="/now" data-astro-cid-sz7xmlte>Now</a> <a href="/changelog" data-astro-cid-sz7xmlte>Changelog</a> <a href="/rss.xml" data-astro-cid-sz7xmlte>RSS</a> </div> <div class="footer-content" data-astro-cid-sz7xmlte> <p class="footer-content--header" data-astro-cid-sz7xmlte>Elsewhere on the internet</p> <a href="https://bsky.app/profile/lurkmoophy.bsky.social" data-astro-cid-sz7xmlte>Bluesky</a> <a href="https://github.com/lurkmoophy" data-astro-cid-sz7xmlte>GitHub</a> <a href="https://www.linkedin.com/in/lukeandrewmurphy/" data-astro-cid-sz7xmlte>LinkedIn</a> <a href="https://www.instagram.com/lurkmoophy/" data-astro-cid-sz7xmlte>Instagram</a> </div> <div class="footer-content" data-astro-cid-sz7xmlte> <p class="footer-content--header" data-astro-cid-sz7xmlte>Contact</p> <a href="mailto:lurkmoophy@gmail.com" data-astro-cid-sz7xmlte>lurkmoophy@gmail.com</a> </div> <div class="footer-bottom" data-astro-cid-sz7xmlte> <span data-astro-cid-sz7xmlte>© 2026 lurkmoophy.</span> <span data-astro-cid-sz7xmlte>Made with sticky tape and dreams.</span> </div> </footer> </section> </body></html>