Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/components/landing/Hero.vue
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ defineProps<{
width="234"
height="234"
class="aspect-square object-cover transition-all duration-300 ease-out group-hover:blur-[2px]"
:class="projectImageAnchorClass(project.imageAnchor)"
:modifiers="projectImageAnchorModifiers(project.imageAnchor)"
:light="project.image!"
:dark="project.imageDark || project.image!"
:alt="project.title"
Expand Down
1 change: 1 addition & 0 deletions app/pages/projects.vue
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ defineOgImage('Portfolio', { title, description })
:dark="project.imageDark || project.image!"
:alt="project.title"
class="object-cover w-full h-48 transition-all duration-300 ease-out group-hover:blur-[2px]"
:class="projectImageAnchorClass(project.imageAnchor)"
/>
<div
class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/40 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
Expand Down
51 changes: 51 additions & 0 deletions app/utils/projectImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Where a project image sits when it has to be cropped.
*
* A centre crop suits a photo and rarely suits a screenshot, where the
* wordmark and the primary action tend to sit against one edge.
*
* An anchor has to be expressed twice, because the two call sites are cropped
* by different things. The home page marquee asks @nuxt/image for a square and
* gets one already cropped, server-side, before CSS has any say — that needs
* `projectImageAnchorModifiers`. The projects page takes the image whole and
* lets `object-cover` crop it in the browser — that needs
* `projectImageAnchorClass`.
*
* The classes are written out in full because Tailwind only emits utilities it
* can find as literal strings; `object-${anchor}` compiles to nothing. The
* corners use sharp's compass names rather than its `'left top'` spelling
* because the modifier is placed in a URL, and IPX answers 500 to the space.
*/
const ANCHORS = {
'center': { class: 'object-center', position: 'center' },
'top': { class: 'object-top', position: 'top' },
'bottom': { class: 'object-bottom', position: 'bottom' },
'left': { class: 'object-left', position: 'left' },
'right': { class: 'object-right', position: 'right' },
'top-left': { class: 'object-top-left', position: 'northwest' },
'top-right': { class: 'object-top-right', position: 'northeast' },
'bottom-left': { class: 'object-bottom-left', position: 'southwest' },
'bottom-right': { class: 'object-bottom-right', position: 'southeast' }
} as const

export type ProjectImageAnchor = keyof typeof ANCHORS

/** The accepted values, in the shape `z.enum()` wants. */
export const projectImageAnchors = Object.keys(ANCHORS) as [
ProjectImageAnchor,
...ProjectImageAnchor[]
]

function resolve(anchor?: string) {
return ANCHORS[anchor as ProjectImageAnchor] ?? ANCHORS.center
}

/** For a crop the browser performs, via `object-cover`. */
export function projectImageAnchorClass(anchor?: string): string {
return resolve(anchor).class
}

/** For a crop @nuxt/image performs while resizing. */
export function projectImageAnchorModifiers(anchor?: string): { position: string } {
return { position: resolve(anchor).position }
}
2 changes: 2 additions & 0 deletions content.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineCollection, defineContentConfig, z } from '@nuxt/content'
import { projectImageAnchors } from './app/utils/projectImage'

const createBaseSchema = () =>
z.object({
Expand Down Expand Up @@ -58,6 +59,7 @@ export default defineContentConfig({
description: z.string().nonempty(),
image: z.string().editor({ input: 'media' }).optional(),
imageDark: z.string().editor({ input: 'media' }).optional(),
imageAnchor: z.enum(projectImageAnchors).optional(),
url: z.string().nonempty(),
tags: z.array(z.string()),
year: z.number()
Expand Down
1 change: 1 addition & 0 deletions content/projects/whats-the-point.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ description:
icon: i-lucide-spade
image: /projects/whats-the-point-light.png
imageDark: /projects/whats-the-point-dark.png
imageAnchor: left
url: "https://wtp.godden.dev/"
tags: ["UI Design", "Front-End Dev"]
year: 2025
Loading