案例研究:lynnandtonic.com 2025 刷新
Case Study: lynnandtonic.com 2025 refresh

原始链接: https://lynnandtonic.com/thoughts/entries/case-study-2025-refresh/

## 2025 作品集更新:一场有趣的实验 在经历了2025年健康上的挑战后,作者旨在进行一次简单且无压力的作品集更新。核心理念围绕着俏皮地颠覆响应式设计——故意*破坏*它。受到拉伸图像和固定宽度网站的限制的启发,该网站现在在浏览器窗口调整大小时会“拉伸”和“压缩”其内容,然后在调整停止时恢复原状。这种效果是通过JavaScript和CSS `scale()` 变换实现的,经过精心管理以避免不必要的翻转并保持可用性。 内容区域固定为436像素,确保一致的拉伸效果,并在较小视口(低于500像素)上恢复完全响应式。在视觉上,该网站从平装书汲取灵感,具有微妙的纹理和章节风格的标题。 除了核心效果之外,更新还包括对链接的焦点状态的改进,以及对作者20年来每年更新作品集的传统的回顾,承认了持久页面布局和可管理年度更改之间的权衡。最终,2025年的作品集是一次轻松的实验,只能通过调整大小来发现,也是在玩转式网页开发中找到乐趣的证明。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 案例研究:lynnandtonic.com 2025 刷新 (lynnandtonic.com) 5 分,由 surprisetalk 1小时前发布 | 隐藏 | 过去 | 收藏 | 讨论 帮助 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请YC | 联系 搜索:
相关文章

原文

I had kind of a weird 2025 health-wise, so when portfolio refresh season rolled around I knew I wanted to keep things simple and avoid stressing during holiday time. So what might be fun and not too complicated?

I usually start by thinking about responsive design and the physical act of resizing the browser. In 2024, I played around with stretching text; maybe I could expand on that and think about content stretching but in an undesirable way? Like when you see an image that’s stretched to fit its container but isn’t maintaining its aspect ratio and it’s all wonky.

This got me thinking about the olden days and that means fixed-width websites. When you resized one, nothing typically happened, though. If you sized bigger, you’d get more blank space around the site and if you sized smaller, you’d get overflow and a horizontal scrollbar. What could it mean for a fixed-width website to be responsive?

I liked the idea of trying to resize a website to make it fill more space, but it just stretches the site like it’s elastic. And when you stop, the content just bounces back to the size it was before. And if you resize it smaller, it squishes it until it’s basically unreadable (until you stop, of course). Resizing is futile—but fun! The grain of this website is polyester.

Here’s a preview of the final effect:

Squash and stretch

To produce the effect I wanted, I needed to use some JavaScript. It’s easier to make images squish and stretch with CSS, but text wants to flow when its container changes size. That’s normally a good thing!

two divs of text content: one is 300px wide and the second is 600px wide; in both divs, the text fills the available space and wraps when it needs to

But this meant I couldn’t just change the width and I needed to use a scale() transform. Something like transform: scale(2,1) will stretch text content so it looks like this:

another two divs: both are 300px wide, but the second one is scaled to 600px wide and it is horizontally stretched out

So to have the site continue to scale() as the browser changes widths, I’d need the value to be dynamically updating. And to calculate what that value should be, I need three other values:

  1. The width of the content container (fixed at 436px, more on why later)
  2. The width of the browser window when resizing begins
  3. The width of the browser window as it’s resizing

So I set up some variables and a ResizeObserver:

// 1. Width of content container
const app = document.querySelector('.app');
const appWidth = app.offsetWidth;
// 2. Width of browser window at start
let windowWidth = window.innerWidth;

const myObserver = new ResizeObserver(entries => {
  entries.forEach(entry => {
    // 3. Width of resizing browser window
    const newWidth = entry.contentRect.width;
    let scaleX = ((newWidth - windowWidth) / appWidth + 1);
    app.style.transform = "scale(" + scaleX + ", 1)";
  });
});

myObserver.observe(document.body);

Let’s breakdown what’s happening in that scaleX. I ultimately want to end up with a number like .567 or 1.23 because that’s what scale() wants. So I start by calculating the percentage the content is changing.

let scaleX = ((newWidth - windowWidth) / appWidth);

I know appWidth is 436px. Let’s say when I start to resize the browser, the windowWidth is 600px and I am making the window bigger so newWidth will be climbing to 602px then 605px etc.

The calculation there becomes:

(605 - 600) / 436 = 0.011467889908257

That small decimal is how much the container is changing and I add a 1 to that to get the scaleX value.

(605 - 600) / 436 + 1 = 1.011467889908257
let scaleX = ((newWidth - windowWidth) / appWidth + 1);
app.style.transform = "scale(" + scaleX + ", 1)";

It’s stretching! But there’s a few issues. When you scale smaller, the scaleX value will eventually become negative which causes the content to flip horizontally, which I don’t want.

I can add a Math.max() to make sure scaleX never goes below a value I set.

let scaleX = (Math.max(0.01,((newWidth - windowWidth) / appWidth + 1)));

Next, I want the site to reset back to the 436px width when I stop resizing the browser. I can add a Timeout and an EventListener that resets the transform so I can scale again.

const observerDebouncers = new WeakMap;

const myObserver = new ResizeObserver(entries => {
  entries.forEach(entry => {
    // Creates timeout
    clearTimeout( observerDebouncers.get( entry.target ));
    observerDebouncers.set( entry.target, setTimeout(() => {
      entry.target.dispatchEvent( new CustomEvent( 'resized' ));
    }, 200));

    const newWidth = entry.contentRect.width;
    let scaleX = (Math.max(0.01,((newWidth - windowWidth) / appWidth + 1)));
    app.style.transform = "scale(" + scaleX + ", 1)";
  });
});

// Resets the transform & windowWidth
body.addEventListener( 'resized', event => {
  windowWidth = window.innerWidth;
  app.style.transform = "scale(1, 1)";
});

And to make the effect feel nice, I added a little bounce transition when the reset happens.

.app {
  transition: transform 100ms cubic-bezier(0.175, 0.885, 0.12, 1.775)
}

I love the silliness of the effect and how, like many years past, you have to resize to discover it. This post about it on Bluesky warms my heart:

So why is the content 436px wide?

The effect works best if the content container is always the same fixed width. If the width is changing along with the stretching, it feels like a mistake. It should feel as fluid and seamless as possible and most desktop browsers don’t let you resize narrower than 500px (at least on MacOS). So with some nice padding for the content, 436px fits well at that smallest size.

To make sure the site is still usable on a phone, once the viewport is below 500px it’s regular full-width responsive again.

The look of it

While keeping things simple, I did want to bring back a bit of texture to the site. Because of the narrow content container, I drew inspiration from printed paperback books. In light mode, the site features a subtle paper texture and in dark mode, it has light dust.

The landing page serves as a table of contents and internal pages feature a “chapter” style header. Light mode keeps a more classic paperback book feel and dark mode is the goth version of that book.

side by side comparison of the About page in light and dark mode

The fonts are Hubano Rough by SimpleBits and Sydonia Atramentiqua by Piotr Wardziukiewicz to add to the printed styling.

Hubano font, an ornate all-caps serif font with curved embellishments Sydonia Atramentiqua, a serif font that looks inky like printed text

A couple other details to mention

Because internal pages have the site nav at the bottom of the page, my skip link—for the first time—skips the content and not to the content.

website with button at the top that says “Jump to navigation”

I especially like the focus states for inline links for this one. They required a little bit of tinkering to get them there. I’m using outline over border to avoid any text shifting, and outline-offset gives the link some better padding. Unfortunately this causes the box-shadow to leak through.

a:focus:focus-visible {
  outline: 4px solid var(--focus-color);
  outline-offset: 3px;
  box-shadow: 6px 6px 0 7px var(--text-color);
}
text link with thick red border and black drop shadow, there is a black outline around the text inside the red border that feels incorrect

Just updating the background-color doesn’t work to cover this up (yellow here to illustrate):

a:focus:focus-visible {
  background-color: yellow;
}
the same link with red border, but the text has a yellow background; the incorrect black outline is still visible

To fix this, I added another box-shadow to obscure the shadow color.

a:focus:focus-visible {
  box-shadow: 0 0 0 3px yellow,
              6px 6px 0 7px var(--text-color);
}
the same link with red border, this time with a second inset yellow border that eliminates the incorrect black outline

So the finishing touch is making that new box-shadow and the background-color the color of the page background and we’re good to go.

a:focus:focus-visible {
  outline: 4px solid var(--focus-color);
  outline-offset: 3px;
  background-color: var(--bg-color);
  box-shadow: 0 0 0 3px var(--bg-color),
              6px 6px 0 7px var(--text-color);
}
same link with red border and black shadow without any yellow or incorrect black outline; also shows the dark mode version with white outline and red shadow

Looking backwards and forwards

2026 will be my portfolio’s 20th refresh. I’ve been thinking about how this yearly change affects what I do and don’t do with the site.

There are a handful of pages that don’t currently get archived with each version: Thoughts, Archive, Gifs, and the 404 page. For Thoughts, it feels overly complicated to maintain posts within the year’s site they were originally created. If you clicked an older post from the current site, it could be confusing to arrive at what feels like another site entirely. There’s probably some build step I could set up to duplicate or skin posts within each site’s theme, but that feels like more than I want to commit to.

Similarly for Archive, I just want the simplicity of one page you can link to and navigate from. And I do not know how to make a conditional 404 page that knows which site version you were trying to reach. I’m sure it’s possible but I’m tired!

On the plus side, this frees me of a lot of worry and work. Each year my primary focus is on the landing, Work, and About pages. It makes it all feel much more achievable. On the down side, knowing those page layouts won’t persist makes it so I don’t ever get too ambitious with those views.

I’ve also been wondering how long I will continue to do this! I guess until I just don’t want to anymore, but I’ll have to make sure that last one is one I really love.

That’s all for 2025! 👋 Thanks for checking it out!

联系我们 contact @ memedata.com