Compare commits
10 commits
aa26e7cd55
...
de0b56ef5b
Author | SHA1 | Date | |
---|---|---|---|
de0b56ef5b | |||
26258249d1 | |||
90edb8db0a | |||
522392cdfc | |||
4bf6f04222 | |||
3698e926ea | |||
8eb41f8fa4 | |||
78b0780e12 | |||
a04446de76 | |||
82b68b9f11 |
|
@ -1,4 +1,4 @@
|
|||
FROM node:latest as node
|
||||
FROM node:latest AS node
|
||||
USER node
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
|
@ -10,3 +10,4 @@ RUN ["npx", "astro", "build"]
|
|||
FROM ghcr.io/static-web-server/static-web-server:latest
|
||||
WORKDIR /
|
||||
COPY --from=node /usr/src/app/dist /public
|
||||
ENV SERVER_REDIRECT_TRAILING_SLASH=false
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { defineConfig } from "astro/config";
|
||||
import { defineConfig, fontProviders } from "astro/config";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import rehypeExternalLinks from "rehype-external-links";
|
||||
import mdx from "@astrojs/mdx";
|
||||
|
@ -31,4 +31,23 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
},
|
||||
redirects: {
|
||||
"/archive": "/projects/archive",
|
||||
},
|
||||
experimental: {
|
||||
fonts: [
|
||||
{
|
||||
provider: fontProviders.fontsource(),
|
||||
name: "Outfit",
|
||||
cssVariable: "--font-outfit",
|
||||
weights: ["100 900"],
|
||||
},
|
||||
{
|
||||
provider: fontProviders.fontsource(),
|
||||
name: "Red Hat Mono",
|
||||
cssVariable: "--font-red-hat-mono",
|
||||
weights: ["300 700"],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
694
package-lock.json
generated
20
package.json
|
@ -13,25 +13,23 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "0.9.4",
|
||||
"@astrojs/mdx": "^4.1.1",
|
||||
"@astrojs/mdx": "^4.2.6",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/sitemap": "3.2.1",
|
||||
"@fontsource-variable/outfit": "^5.2.5",
|
||||
"@fontsource-variable/red-hat-mono": "^5.2.5",
|
||||
"@tailwindcss/vite": "^4.0.13",
|
||||
"astro": "^5.4.3",
|
||||
"@astrojs/sitemap": "3.3.1",
|
||||
"@tailwindcss/vite": "^4.1.5",
|
||||
"astro": "^5.7.10",
|
||||
"astro-icon": "^1.1.5",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"tailwindcss": "^4.0.13",
|
||||
"typescript": "^5.8.2"
|
||||
"tailwindcss": "^4.1.5",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/node": "^22.15.3",
|
||||
"npm-check-updates": "^18.0.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"npm-check-updates": "^17.1.15"
|
||||
"prettier-plugin-tailwindcss": "^0.6.11"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,15 +9,28 @@ import RelatedArticles from "@components/RelatedArticles.astro";
|
|||
const { article, isPost = false } = Astro.props;
|
||||
const { Content } = await article.render();
|
||||
|
||||
let datesMatch = false;
|
||||
if (article.data.date.getTime() == article.data.updated?.getTime()) {
|
||||
datesMatch = true;
|
||||
interface NameAndUrl {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const listFormatter = new Intl.ListFormat("en-GB", {
|
||||
style: "long",
|
||||
type: "conjunction",
|
||||
});
|
||||
function formatNamesWithLinks(namesAndUrls: NameAndUrl[]) {
|
||||
const listFormatter = new Intl.ListFormat("en-GB", {
|
||||
style: "long",
|
||||
type: "conjunction",
|
||||
});
|
||||
|
||||
const linkedNames = namesAndUrls.map((item: NameAndUrl) => {
|
||||
return `<a href="${item.url}" rel="nofollow" target="_blank" class="underline hover:no-underline">${item.name}</a>`;
|
||||
});
|
||||
|
||||
return listFormatter.format(linkedNames);
|
||||
}
|
||||
|
||||
let formattedList: string | null = null;
|
||||
if (article.data.extraAuthors && article.data.extraAuthors.length !== 0) {
|
||||
formattedList = formatNamesWithLinks(article.data.extraAuthors);
|
||||
}
|
||||
---
|
||||
|
||||
<Layout
|
||||
|
@ -41,7 +54,7 @@ const listFormatter = new Intl.ListFormat("en-GB", {
|
|||
<div class="flex items-center gap-2">
|
||||
<Icon name="mdi:calendar" />
|
||||
{
|
||||
datesMatch ? (
|
||||
!article.data.updated ? (
|
||||
<p title="Date">
|
||||
<FormattedDate date={article.data.date} />
|
||||
</p>
|
||||
|
@ -70,9 +83,7 @@ const listFormatter = new Intl.ListFormat("en-GB", {
|
|||
article.data.extraAuthors ? (
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="mdi:account-plus" />
|
||||
<p title="Collaborators">
|
||||
{listFormatter.format(article.data.extraAuthors)}
|
||||
</p>
|
||||
<p title="Collaborators" set:html={formattedList} />
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
@ -80,7 +91,7 @@ const listFormatter = new Intl.ListFormat("en-GB", {
|
|||
article.data.tags ? (
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Icon name="mdi:tag" />
|
||||
{article.data.tags.map((tag: any) => (
|
||||
{article.data.tags.map((tag: string) => (
|
||||
<a
|
||||
href={`/tags/${createSlug(tag)}`}
|
||||
class="underline hover:no-underline"
|
||||
|
|
|
@ -9,7 +9,7 @@ const { href, link } = Astro.props;
|
|||
|
||||
<a href={`${href}`}>
|
||||
<div
|
||||
class="bg-button text-secondary hover:bg-button-active flex w-fit flex-row items-center gap-1 justify-self-center rounded-full px-3 py-1.5 text-center text-sm font-medium text-nowrap capitalize transition-colors duration-300"
|
||||
class="bg-button text-secondary hover:bg-button-active flex w-fit flex-row items-center gap-1 justify-self-center rounded-md px-3 py-1.5 text-center text-sm font-medium text-nowrap capitalize transition-colors duration-300"
|
||||
>
|
||||
{link}
|
||||
</div>
|
||||
|
|
91
src/components/CvProjects.astro
Normal file
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
import Link from "@components/Link.astro";
|
||||
import { createSlug } from "@lib/utils";
|
||||
|
||||
const projects = [
|
||||
{
|
||||
id: 1,
|
||||
name: "MUST FIND BEANS",
|
||||
description:
|
||||
"A fast-paced first person shooter set following the realization that you’re all out of beans. The problem is, you’re nearing the end of cooking all the other items and you can’t just not have them. Without beans, the day just won’t be started off right.",
|
||||
tags: ["Godot", "Blender", "GIMP", "Steamworks"],
|
||||
link: "/projects/must-find-beans",
|
||||
done: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "troylusty.com",
|
||||
description:
|
||||
"My personal website made using Astro as a way to show off my portfolio of work and display blog posts.",
|
||||
tags: [
|
||||
"Astro",
|
||||
"Tailwind CSS",
|
||||
"TypeScript",
|
||||
"Self-hosted Forgejo Actions",
|
||||
"Docker",
|
||||
],
|
||||
link: "https://code.troylusty.com/troy/troylusty.com",
|
||||
done: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Artwork",
|
||||
description:
|
||||
"A collection of digital artwork created with a variety of tools.",
|
||||
tags: ["Blender", "Cinema 4D", "DaVinci Resolve"],
|
||||
link: "/projects",
|
||||
done: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Packard",
|
||||
description:
|
||||
"Packard is a simple terminal based RSS aggregator meant to allow you to take a quick glance at what’s occurring in topics you care about.",
|
||||
tags: ["Rust", "Tokio", "Clap", "NixOS Flake"],
|
||||
link: "/projects/packard",
|
||||
done: true,
|
||||
},
|
||||
];
|
||||
const sortedProjects = [...projects].sort((a, b) => a.id - b.id);
|
||||
---
|
||||
|
||||
<ol class="grid grid-cols-1 gap-3 pl-0 md:grid-cols-2 print:grid-cols-2">
|
||||
{
|
||||
sortedProjects.map((project) => (
|
||||
<li class="flex flex-col overflow-hidden pl-0">
|
||||
<div class="flex flex-col space-y-1.5">
|
||||
<div class="space-y-1">
|
||||
<>
|
||||
<h3 class="mt-0 text-base font-semibold tracking-tight">
|
||||
<Link
|
||||
href={project.link}
|
||||
class="text-secondary inline-flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{project.done ? (
|
||||
<span class="mr-1 h-1 w-1 rounded-full bg-green-500" />
|
||||
) : (
|
||||
<span class="mr-1 h-1 w-1 rounded-full bg-amber-500" />
|
||||
)}
|
||||
{project.name}
|
||||
</Link>
|
||||
</h3>
|
||||
<p class="text-secondary/70 text-xs">{project.description}</p>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto flex text-sm text-pretty">
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
{project.tags.map((tag) => (
|
||||
<a
|
||||
href={`/tags/${createSlug(tag)}`}
|
||||
class="bg-button text-secondary hover:bg-button-active flex w-fit flex-row items-center gap-1 justify-self-center rounded-sm px-2 py-1 text-center font-sans text-[10px] font-light text-nowrap capitalize no-underline transition-colors duration-300"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ol>
|
33
src/components/Gallery.astro
Normal file
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import type { ImageMetadata } from "astro";
|
||||
|
||||
interface Item {
|
||||
src: ImageMetadata;
|
||||
alt: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items: Item[];
|
||||
}
|
||||
|
||||
const { items } = Astro.props as Props;
|
||||
---
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
{
|
||||
items.map((item: Item) => (
|
||||
<div class="flex-col overflow-hidden rounded-sm">
|
||||
{item.src && (
|
||||
<Image
|
||||
src={item.src}
|
||||
alt={item.alt}
|
||||
title={item.alt}
|
||||
loading="eager"
|
||||
class="mt-0 mb-0 h-full max-h-[90svh] w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
|
@ -2,6 +2,7 @@
|
|||
import { SITE } from "@consts";
|
||||
import gradient from "../../public/assets/gradient.avif";
|
||||
import { ClientRouter } from "astro:transitions";
|
||||
import { Font } from "astro:assets";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
@ -20,9 +21,6 @@ let { tags } = Astro.props;
|
|||
if (typeof tags !== "undefined") {
|
||||
tags = SITE.KEYWORDS.concat(tags);
|
||||
}
|
||||
|
||||
import outfit from "@fontsource-variable/outfit/files/outfit-latin-wght-normal.woff2?url";
|
||||
import redhatmono from "@fontsource-variable/red-hat-mono/files/red-hat-mono-latin-wght-normal.woff2?url";
|
||||
---
|
||||
|
||||
<head>
|
||||
|
@ -96,18 +94,6 @@ import redhatmono from "@fontsource-variable/red-hat-mono/files/red-hat-mono-lat
|
|||
<ClientRouter />
|
||||
|
||||
<!-- Font Preload -->
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
href={outfit}
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
href={redhatmono}
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<Font cssVariable="--font-outfit" preload />
|
||||
<Font cssVariable="--font-red-hat-mono" preload />
|
||||
</head>
|
||||
|
|
|
@ -5,26 +5,26 @@
|
|||
<span class="text-nowrap">Troy Lusty</span>
|
||||
<span class="text-tertiary text-pretty">Digital Designer</span>
|
||||
</h1>
|
||||
<div
|
||||
<p
|
||||
class="text-secondary/70 animate-reveal text-lg font-medium opacity-0 [animation-delay:0.1s]"
|
||||
>
|
||||
I am in my final year of studying on a Game Arts and Design BA (Hons) degree
|
||||
and on the side I manage online operations for a family run
|
||||
<a
|
||||
href="/projects/camouflage-store"
|
||||
class="underline decoration-2 underline-offset-2 hover:no-underline"
|
||||
class="text-secondary underline decoration-2 underline-offset-2 hover:no-underline"
|
||||
>outdoor apparel business</a
|
||||
>.
|
||||
</div>
|
||||
<div
|
||||
</p>
|
||||
<p
|
||||
class="text-tertiary animate-reveal text-lg font-medium opacity-0 [animation-delay:0.2s]"
|
||||
>
|
||||
Think I could help with a project you're working on?
|
||||
<a
|
||||
href="mailto:hello@troylusty.com"
|
||||
class="ml-1 w-fit rounded-full bg-lime-500/20 px-2 py-1 text-sm text-lime-800 ring ring-lime-500 transition-colors duration-300 hover:bg-lime-500/30 dark:text-lime-200"
|
||||
class="ml-1 w-fit rounded-sm bg-lime-500/20 px-2 py-1 text-sm text-lime-800 ring ring-lime-500 transition-colors duration-300 hover:bg-lime-500/30 dark:text-lime-200"
|
||||
>
|
||||
Send me a message
|
||||
</a>
|
||||
</div>
|
||||
</p>
|
||||
</section>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div
|
||||
class="prose prose-neutral dark:prose-invert prose-lg prose-img:max-h-[90svh] prose-img:rounded-sm prose-img:w-auto prose-img:max-w-full prose-video:max-h-[90svh] prose-video:rounded-sm prose-video:w-auto prose-video::max-w-full prose-a:hover:no-underline"
|
||||
class="prose prose-neutral dark:prose-invert prose-lg prose-img:max-h-[90svh] prose-img:rounded-sm prose-img:w-auto prose-img:max-w-full prose-video:max-h-[90svh] prose-video:rounded-sm prose-video:w-auto prose-video::max-w-full prose-a:hover:no-underline prose-a:decoration-2 prose-a:underline-offset-2"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,7 @@ const next = items[(index + 1) % items.length];
|
|||
|
||||
{
|
||||
items.length > 1 ? (
|
||||
<div class="animate-reveal mx-auto flex w-full flex-col-reverse items-center justify-between gap-4 opacity-0 [animation-delay:0.1s] md:flex-row">
|
||||
<div class="animate-reveal mx-auto flex w-full flex-row flex-wrap items-center justify-between gap-2 opacity-0 [animation-delay:0.1s]">
|
||||
<Button
|
||||
href={`/${prev.collection}/${prev.slug}`}
|
||||
link={`Previous: ${prev.data.title}`}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
---
|
||||
import FormattedDate from "@components/FormattedDate.astro";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
type Props = {
|
||||
collection: any;
|
||||
collection: CollectionEntry<"posts" | "projects">;
|
||||
};
|
||||
|
||||
const { collection } = Astro.props;
|
||||
|
@ -18,10 +20,10 @@ const { collection } = Astro.props;
|
|||
<h3 class="text-secondary mb-1 text-lg font-semibold text-nowrap">
|
||||
{collection.data.title}
|
||||
</h3>
|
||||
<FormattedDate
|
||||
date={collection.data.date}
|
||||
className="text-tertiary block text-nowrap"
|
||||
/>
|
||||
<div class="text-tertiary flex items-center gap-2">
|
||||
<Icon name="mdi:calendar" />
|
||||
<FormattedDate date={collection.data.date} />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-tertiary text-pretty">{collection.data.description}</p>
|
||||
</article>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
type Props = {
|
||||
collection: any;
|
||||
collection: CollectionEntry<"projects">;
|
||||
};
|
||||
|
||||
const { collection } = Astro.props;
|
||||
|
@ -21,6 +22,13 @@ const { collection } = Astro.props;
|
|||
fit="cover"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="relative opacity-0 transition-all delay-100 duration-300 ease-in-out group-hover:opacity-100"
|
||||
>
|
||||
<p class="absolute right-5 bottom-5 font-medium">
|
||||
{collection.data.title}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
</li>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
interface Props {
|
||||
images: CollectionEntry<"projects">[];
|
||||
interval?: number;
|
||||
images: any;
|
||||
}
|
||||
|
||||
const { interval = 3000, images } = Astro.props;
|
||||
|
@ -30,6 +31,11 @@ const { interval = 3000, images } = Astro.props;
|
|||
class="h-full w-full rounded-sm object-cover transition-all duration-300 group-hover:brightness-50"
|
||||
loading="eager"
|
||||
/>
|
||||
<div class="relative opacity-0 transition-all delay-100 duration-300 ease-in-out group-hover:opacity-100">
|
||||
<p class="absolute right-5 bottom-5 font-medium">
|
||||
{image.data.title}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
))
|
||||
|
|
|
@ -34,6 +34,11 @@ export const SITE: Site = {
|
|||
href: "/sitemap-index.xml",
|
||||
icon: "mdi:sitemap",
|
||||
},
|
||||
{
|
||||
name: "Curriculum vitae",
|
||||
href: "/cv",
|
||||
icon: "mdi:trophy",
|
||||
},
|
||||
{
|
||||
name: "Email",
|
||||
href: "mailto:hello@troylusty.com",
|
||||
|
@ -59,10 +64,6 @@ export const SITE: Site = {
|
|||
name: "Posts",
|
||||
href: "/posts",
|
||||
},
|
||||
{
|
||||
name: "CV",
|
||||
href: "/cv",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -70,25 +71,6 @@ export const HOME: Metadata = {
|
|||
TITLE: "Troy Lusty",
|
||||
DESCRIPTION:
|
||||
"Hi, my name is Troy and I'm a student 3D artist currently studying in my second year of an FdA Games and Interactive Design course in the UK.",
|
||||
HOMESETTINGS: {
|
||||
NUM_POSTS_ON_HOMEPAGE: 2,
|
||||
NUM_PROJECTS_ON_HOMEPAGE: 6,
|
||||
},
|
||||
};
|
||||
|
||||
export const CV: Metadata = {
|
||||
TITLE: "Troy Lusty",
|
||||
DESCRIPTION: "Curriculum vitae.",
|
||||
};
|
||||
|
||||
export const POSTS: Metadata = {
|
||||
TITLE: "Posts",
|
||||
DESCRIPTION: "A collection of articles on topics I am passionate about.",
|
||||
};
|
||||
|
||||
export const WORK: Metadata = {
|
||||
TITLE: "Work",
|
||||
DESCRIPTION: "Where I have worked and what I have done.",
|
||||
};
|
||||
|
||||
export const PROJECTS: Metadata = {
|
||||
|
@ -96,3 +78,18 @@ export const PROJECTS: Metadata = {
|
|||
DESCRIPTION:
|
||||
"A collection of my projects, with links to repositories and demos.",
|
||||
};
|
||||
|
||||
export const POSTS: Metadata = {
|
||||
TITLE: "Posts",
|
||||
DESCRIPTION: "A collection of articles on topics I am passionate about.",
|
||||
};
|
||||
|
||||
export const ABOUT: Metadata = {
|
||||
TITLE: "About",
|
||||
DESCRIPTION: "About me.",
|
||||
};
|
||||
|
||||
export const CV: Metadata = {
|
||||
TITLE: "Troy Lusty",
|
||||
DESCRIPTION: "Curriculum vitae.",
|
||||
};
|
||||
|
|
|
@ -16,7 +16,14 @@ const posts = defineCollection({
|
|||
alt: z.string(),
|
||||
}),
|
||||
tags: z.array(z.string()),
|
||||
extraAuthors: z.array(z.string()).optional(),
|
||||
extraAuthors: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
url: z.string().url(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
categories: z.array(z.string()),
|
||||
})
|
||||
.merge(rssSchema),
|
||||
|
@ -37,11 +44,18 @@ const projects = defineCollection({
|
|||
alt: z.string(),
|
||||
}),
|
||||
tags: z.array(z.string()),
|
||||
extraAuthors: z.array(z.string()).optional(),
|
||||
extraAuthors: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
url: z.string().url(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
categories: z.array(z.string()),
|
||||
featured: z.boolean().optional(),
|
||||
collection: z.boolean().optional(),
|
||||
includeHero: z.boolean().optional(),
|
||||
rank: z.number().positive().optional(),
|
||||
})
|
||||
.merge(rssSchema),
|
||||
});
|
||||
|
|
212
src/content/posts/website/index.mdx
Normal file
|
@ -0,0 +1,212 @@
|
|||
---
|
||||
title: "Website"
|
||||
date: 2025-04-25
|
||||
description: "An overview of what I am using to host my digital content."
|
||||
image:
|
||||
url: "showcase.webp"
|
||||
alt: "Website showcase"
|
||||
categories: ["personal"]
|
||||
tags: ["self-host", "forgejo", "docker", "vps"]
|
||||
draft: true
|
||||
---
|
||||
|
||||
This post will outline my workflow of using a self-hosted Forgejo instance and Actions runner to automatically deploy my personal site and any software releases, all without having to rely on another provider.
|
||||
|
||||

|
||||
|
||||
## Steps
|
||||
|
||||
### Private image access login?
|
||||
|
||||
```sh
|
||||
echo '<token>' | docker login code.troylusty.com -u troy --password-stdin
|
||||
```
|
||||
|
||||
```sh
|
||||
echo $(htpasswd -nB user) | sed -e s/\\$/\\$\\$/g
|
||||
```
|
||||
|
||||
### Aliases for updating VPS and pruning Docker
|
||||
|
||||
```sh
|
||||
echo 'alias dockerclean="docker system prune -a --volumes"' >> .bashrc
|
||||
echo 'alias updateall="sudo apt update && sudo apt upgrade && sudo apt autoremove"' >> .bashrc
|
||||
```
|
||||
|
||||
Thanks to [Tech Tales](https://tech-tales.blog/posts/2025/01-forgejo-runner-update) for the clean instructions on how to setup an Actions runner with Forgejo.
|
||||
|
||||
```sh
|
||||
docker compose run --rm forgejo-runner 'forgejo-runner' 'generate-config' > forgejo-runner/config.yml
|
||||
```
|
||||
|
||||
Setup `container.docker_host: "unix:///var/run/docker.sock"` and `container.network: "forgejo"` and any labels such as `runner.labels: ["ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"]`
|
||||
|
||||
```
|
||||
docker compose run --rm -it forgejo-runner 'forgejo-runner' 'register'
|
||||
```
|
||||
|
||||
Input `http://forgejo:3000` as the domain since forgejo is the container name in Docker and port 3000 is its relevant port.
|
||||
|
||||
## Docker compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:latest
|
||||
container_name: traefik
|
||||
command:
|
||||
- "--providers.docker"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entryPoints.websecure.address=:443"
|
||||
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
|
||||
- "--certificatesresolvers.myresolver.acme.email=traefik@troylusty.com"
|
||||
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
|
||||
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
|
||||
- "--ping=true"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
- "traefik.http.middlewares.securityHeaders.headers.stsSeconds=31536000"
|
||||
- "traefik.http.middlewares.securityHeaders.headers.stsIncludeSubdomains=true"
|
||||
- "traefik.http.middlewares.securityHeaders.headers.frameDeny=true"
|
||||
- "traefik.http.middlewares.securityHeaders.headers.contentTypeNosniff=true"
|
||||
- "traefik.http.middlewares.securityHeaders.headers.contentSecurityPolicy=default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests"
|
||||
- "traefik.http.middlewares.securityHeaders.headers.referrerPolicy=no-referrer"
|
||||
- "traefik.http.middlewares.securityHeaders.headers.permissionsPolicy=accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(self), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), unload=()"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- letsencrypt:/letsencrypt
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- traefik
|
||||
healthcheck:
|
||||
test: ["CMD", "traefik", "healthcheck", "--ping"]
|
||||
depends_on:
|
||||
watchtower:
|
||||
condition: service_healthy
|
||||
|
||||
watchtower:
|
||||
image: containrrr/watchtower:latest
|
||||
command: --label-enable --interval 1800 --rolling-restart --cleanup --remove-volumes
|
||||
container_name: watchtower
|
||||
networks:
|
||||
- traefik
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /home/troy/.docker/config.json:/config.json
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "/watchtower", "--health-check"]
|
||||
|
||||
personalsite:
|
||||
image: code.troylusty.com/troy/troylusty.com:latest
|
||||
container_name: personalsite
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.personalsite.rule=Host(`troylusty.com`)"
|
||||
- "traefik.http.routers.personalsite.entrypoints=websecure"
|
||||
- "traefik.http.routers.personalsite.tls.certresolver=myresolver"
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
- "traefik.http.routers.personalsite.middlewares=securityHeaders"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- traefik
|
||||
depends_on:
|
||||
traefik:
|
||||
condition: service_healthy
|
||||
|
||||
zolapress:
|
||||
image: code.troylusty.com/troy/zolapress:latest
|
||||
container_name: zolapress
|
||||
profiles:
|
||||
- donotstart
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.zolapress.rule=Host(`edu.troylusty.com`)"
|
||||
- "traefik.http.routers.zolapress.entrypoints=websecure"
|
||||
- "traefik.http.routers.zolapress.tls.certresolver=myresolver"
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
- "traefik.http.middlewares.auth.basicauth.users=troy:$$2y$$05$$fgVNzDsxXDq4co3aTh/OMOKZdLzUiM9XPEU5DXCivc9sYUZy/oq1W"
|
||||
- "traefik.http.routers.zolapress.middlewares=securityHeaders, auth"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- traefik
|
||||
depends_on:
|
||||
traefik:
|
||||
condition: service_healthy
|
||||
|
||||
unduck:
|
||||
image: code.troylusty.com/troy/unduck:latest
|
||||
container_name: unduck
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.unduck.rule=Host(`unduck.troylusty.com`)"
|
||||
- "traefik.http.routers.unduck.entrypoints=websecure"
|
||||
- "traefik.http.routers.unduck.tls.certresolver=myresolver"
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
- "traefik.http.routers.unduck.middlewares=securityHeaders"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- traefik
|
||||
depends_on:
|
||||
traefik:
|
||||
condition: service_healthy
|
||||
|
||||
forgejo:
|
||||
image: codeberg.org/forgejo/forgejo:10
|
||||
container_name: forgejo
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- traefik
|
||||
- forgejo
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.forgejo.rule=Host(`code.troylusty.com`)"
|
||||
- "traefik.http.routers.forgejo.entrypoints=websecure"
|
||||
- "traefik.http.routers.forgejo.tls.certresolver=myresolver"
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
- "traefik.http.routers.forgejo.middlewares=securityHeaders"
|
||||
- "traefik.http.services.forgejo.loadbalancer.server.port=3000"
|
||||
- "traefik.docker.network=traefik"
|
||||
volumes:
|
||||
- ./forgejo:/data
|
||||
ports:
|
||||
- "2222:22"
|
||||
depends_on:
|
||||
traefik:
|
||||
condition: service_healthy
|
||||
|
||||
forgejo-runner:
|
||||
image: code.forgejo.org/forgejo/runner:6.0.1
|
||||
container_name: forgejo-runner
|
||||
user: 0:0
|
||||
depends_on:
|
||||
forgejo:
|
||||
condition: service_started
|
||||
networks:
|
||||
- forgejo
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
volumes:
|
||||
- ./forgejo-runner:/data
|
||||
- ./forgejo-runner/config.yml:/data/config.yml
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
restart: unless-stopped
|
||||
command: forgejo-runner -c /data/config.yml daemon
|
||||
|
||||
networks:
|
||||
traefik:
|
||||
external: false
|
||||
name: traefik
|
||||
forgejo:
|
||||
external: false
|
||||
name: forgejo
|
||||
|
||||
volumes:
|
||||
letsencrypt:
|
||||
```
|
BIN
src/content/posts/website/showcase.webp
Normal file
After Width: | Height: | Size: 160 KiB |
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
title: "3D Package Design"
|
||||
description: "3D Package Design inspired by the work of Derek Elliott."
|
||||
date: 2020-08-16
|
||||
updated: 2020-08-16
|
||||
image:
|
||||
{ url: "troy-lusty-3d-package-design.avif", alt: "3D package design frame" }
|
||||
tags: ["blender"]
|
||||
categories: ["personal"]
|
||||
---
|
||||
|
||||
import glowing_box_animation from "glowing-box-animation.webm";
|
||||
|
||||
3D Package Design inspired from [video](https://www.youtube.com/watch?v=4SRwODk0oOU) by Derek Elliott.
|
||||
This was the final product from my first attempt at some simple animation within Blender done sometime in August of 2020.
|
||||
|
||||
<video preload="metadata" loop muted controls>
|
||||
<source src={glowing_box_animation} type="video/webm" />
|
||||
</video>
|
||||
|
||||
_Animation_
|
||||
|
||||

|
Before Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 103 KiB |
BIN
src/content/projects/a-long-way-down/Screenshot from 2023-03-01 20-02-00.jpg
Executable file
After Width: | Height: | Size: 94 KiB |
BIN
src/content/projects/a-long-way-down/Screenshot from 2023-03-07 15-30-43.png
Executable file
After Width: | Height: | Size: 2.2 MiB |
After Width: | Height: | Size: 2.5 MiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 13 KiB |
BIN
src/content/projects/a-long-way-down/alwd-img1.jpg
Executable file
After Width: | Height: | Size: 133 KiB |
Before Width: | Height: | Size: 13 KiB |
BIN
src/content/projects/a-long-way-down/alwd-img2.jpg
Executable file
After Width: | Height: | Size: 113 KiB |
Before Width: | Height: | Size: 20 KiB |
BIN
src/content/projects/a-long-way-down/alwd-img3.jpg
Executable file
After Width: | Height: | Size: 132 KiB |
Before Width: | Height: | Size: 10 KiB |
BIN
src/content/projects/a-long-way-down/alwd-img4.jpg
Executable file
After Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 15 KiB |
BIN
src/content/projects/a-long-way-down/alwd-img5.jpg
Executable file
After Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 12 KiB |
BIN
src/content/projects/a-long-way-down/alwd-img6.jpg
Executable file
After Width: | Height: | Size: 133 KiB |
Before Width: | Height: | Size: 7.1 KiB |
|
@ -1,66 +1,70 @@
|
|||
---
|
||||
title: "A Long Way Down (Demo)"
|
||||
title: "A Long Way Down"
|
||||
description: "A short, atmospheric linear adventure created for my FdA Games and Interactive Design degree."
|
||||
date: 2023-05-11
|
||||
updated: 2023-05-11
|
||||
featured: true
|
||||
image: { url: "alwd-img1.avif", alt: "A Long Way Down Intro Showcase" }
|
||||
image: { url: "alwd-img1.jpg", alt: "A Long Way Down Intro Showcase" }
|
||||
tags: ["unreal engine", "blender", "inkscape"]
|
||||
categories: ["education"]
|
||||
includeHero: true
|
||||
extraAuthors: ["Sam Griffiths"]
|
||||
extraAuthors: [{ name: "Sam Griffiths", url: "https://samgriffiths.dev" }]
|
||||
---
|
||||
|
||||
import Gallery from "@components/Gallery.astro";
|
||||
|
||||
import image1 from "alwd-img1.jpg";
|
||||
import image2 from "alwd-img2.jpg";
|
||||
import image3 from "alwd-img3.jpg";
|
||||
import image4 from "alwd-img4.jpg";
|
||||
import image5 from "alwd-img5.jpg";
|
||||
import image6 from "alwd-img6.jpg";
|
||||
import image7 from "29ff3-alwd-introarea-24042023.jpg";
|
||||
import image8 from "Screenshot_20230402_222043.png";
|
||||
import image9 from "Screenshot from 2023-03-07 15-30-43.png";
|
||||
import image10 from "Screenshot from 2023-03-01 20-02-00.jpg";
|
||||
|
||||
import alongwaydown_demo_walkthrough from "alongwaydown-demo-walkthrough.webm";
|
||||
|
||||
A Long Way Down is a short, atmospheric linear adventure created alongside my friend [Sam](https://samgriffiths.dev) as a project for our FdA Games and Interactive Design degree. It is the follow up project to our previous work: [Nightmare](/projects/nightmare). Currently the [demo](https://samandtroy.itch.io/alongwaydown) is available on Itch.io.
|
||||
A Long Way Down is a short, atmospheric linear adventure created alongside my friend Sam as a project for our FdA Games and Interactive Design degree. My role was art direction including the lighting, level design, and majority of asset creation for the project. This is the follow up project to our previous work: [Nightmare](#nightmare), and was made to be an evolutionary improvement of it. Currently the [demo](https://samandtroy.itch.io/alongwaydown) is available to download and play on Itch.io.
|
||||
|
||||
<video preload="metadata" controls>
|
||||
<video preload="metadata" poster={image1.src} controls>
|
||||
<source src={alongwaydown_demo_walkthrough} type="video/webm" />
|
||||
</video>
|
||||
|
||||
_Demo walkthrough_
|
||||
<Gallery
|
||||
items={[
|
||||
{ src: image1, alt: "A Long Way Down Intro Showcase" },
|
||||
{ src: image2, alt: "A Long Way Down Forest Showcase" },
|
||||
{ src: image3, alt: "A Long Way Down Tree Bridge Showcase" },
|
||||
{ src: image4, alt: "A Long Way Down Swamp Showcase" },
|
||||
{ src: image5, alt: "A Long Way Down Final Climb" },
|
||||
{ src: image6, alt: "A Long Way Down Cliff Jump" },
|
||||
{ src: image7, alt: "Alternate night lighting idea" },
|
||||
{ src: image8, alt: "Old colour grade on forest section" },
|
||||
{ src: image9, alt: "Early environment stage" },
|
||||
{ src: image10, alt: "Initial style experiments" },
|
||||
]}
|
||||
/>
|
||||
|
||||

|
||||
## Nightmare
|
||||
|
||||
_Intro_
|
||||
import image11 from "troy-lusty-nightmare.avif";
|
||||
import image12 from "troy-lusty-nightmare-frame-1182.avif";
|
||||
import image13 from "troy-lusty-highresscreenshot00000.png";
|
||||
import image14 from "troy-lusty-untitled.png";
|
||||
|
||||

|
||||
import nightmare from "nightmare.webm";
|
||||
|
||||
_Forest_
|
||||
This is the environment I created in collaboration with Sam Griffiths for our Unit 8 - Developing a Creative Media Production Project - Final Major Project which concluded 2021-04-21. I was in charge of the art side of the project including asset production, composition, and lighting. Additionally, I created this short cinematic in Unreal Engine's cinematic level sequencer by keyframing the transform values and focus distance of a camera.
|
||||
|
||||

|
||||
<video preload="metadata" muted controls>
|
||||
<source src={nightmare} type="video/webm" />
|
||||
</video>
|
||||
|
||||
_Tree bridge_
|
||||
|
||||

|
||||
|
||||
_Swamp_
|
||||
|
||||

|
||||
|
||||
_Final climb_
|
||||
|
||||

|
||||
|
||||
_Cliff jump_
|
||||
|
||||

|
||||
|
||||
_Alternate night lighting idea_
|
||||
|
||||

|
||||
|
||||
_Old colour grade on forest section_
|
||||
|
||||

|
||||
|
||||
_Early environment stage_
|
||||
|
||||

|
||||
|
||||
_Initial style experiments_
|
||||
|
||||
### External links
|
||||
|
||||
[Itch.io page](https://samandtroy.itch.io/alongwaydown)
|
||||
<Gallery
|
||||
items={[
|
||||
{ src: image11, alt: "Frame 0600" },
|
||||
{ src: image12, alt: "Frame 1182" },
|
||||
{ src: image13, alt: "Development image 1" },
|
||||
{ src: image14, alt: "Development image 2" },
|
||||
]}
|
||||
/>
|
||||
|
|
After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
BIN
src/content/projects/a-long-way-down/troy-lusty-untitled.png
Normal file
After Width: | Height: | Size: 2.4 MiB |
BIN
src/content/projects/archive/2022-01-06.jpg
Normal file
After Width: | Height: | Size: 879 KiB |
BIN
src/content/projects/archive/2022-01-27.jpg
Normal file
After Width: | Height: | Size: 296 KiB |
BIN
src/content/projects/archive/2022-03-27.png
Normal file
After Width: | Height: | Size: 2.6 MiB |
BIN
src/content/projects/archive/2022-03-28.jpg
Normal file
After Width: | Height: | Size: 3.3 MiB |
BIN
src/content/projects/archive/2022-05-17.jpg
Normal file
After Width: | Height: | Size: 374 KiB |
BIN
src/content/projects/archive/2022-09-26.jpg
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
src/content/projects/archive/2023-01-05.jpg
Normal file
After Width: | Height: | Size: 174 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
BIN
src/content/projects/archive/2023-10-12.jpg
Normal file
After Width: | Height: | Size: 199 KiB |
BIN
src/content/projects/archive/2023-11-02.jpg
Normal file
After Width: | Height: | Size: 314 KiB |
BIN
src/content/projects/archive/2023-11-23.jpg
Normal file
After Width: | Height: | Size: 329 KiB |
BIN
src/content/projects/archive/2024-02-15.jpg
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
src/content/projects/archive/2024-04-01.jpg
Normal file
After Width: | Height: | Size: 808 KiB |
BIN
src/content/projects/archive/2024-07-19.jpg
Normal file
After Width: | Height: | Size: 191 KiB |
BIN
src/content/projects/archive/2025-01-24.jpg
Normal file
After Width: | Height: | Size: 494 KiB |
BIN
src/content/projects/archive/datsun.jpg
Normal file
After Width: | Height: | Size: 306 KiB |
Before Width: | Height: | Size: 11 MiB After Width: | Height: | Size: 8.9 MiB |
BIN
src/content/projects/archive/forestfire.jpg
Normal file
After Width: | Height: | Size: 20 MiB |
113
src/content/projects/archive/index.mdx
Normal file
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
title: "Archive"
|
||||
description: "A collection of smaller, unfinished, or historic personal works."
|
||||
date: 2021-01-15
|
||||
featured: true
|
||||
image: { url: "2023-11-23.jpg", alt: "Ugolino and His Sons lighting" }
|
||||
tags:
|
||||
[
|
||||
"blender",
|
||||
"unreal engine",
|
||||
"davinci resolve",
|
||||
"photoshop",
|
||||
"gimp",
|
||||
"affinity photo",
|
||||
]
|
||||
categories: ["personal"]
|
||||
---
|
||||
|
||||
import Gallery from "@components/Gallery.astro";
|
||||
|
||||
Whilst not all of these pieces are large enough to have their own project page, I still think they showcase what I am interested in and it didn't feel right to exclude them entirely. This page receives updates periodically.
|
||||
|
||||
import video2023_04_08 from "./2023-04-08.webm";
|
||||
import video2023_02_08 from "./2023-02-08.webm";
|
||||
import video2023_01_08 from "./2023-01-08.webm";
|
||||
import video2023_07_19 from "./2023-07-19.webm";
|
||||
|
||||
<div class="mb-6 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<video
|
||||
preload="metadata"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
class="mt-0 mb-0 aspect-square h-full max-h-[90svh] w-full object-cover"
|
||||
>
|
||||
<source src={video2023_04_08} type="video/webm" />
|
||||
</video>
|
||||
<video
|
||||
preload="metadata"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
class="mt-0 mb-0 aspect-square h-full max-h-[90svh] w-full object-cover"
|
||||
>
|
||||
<source src={video2023_02_08} type="video/webm" />
|
||||
</video>
|
||||
<video
|
||||
preload="metadata"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
class="mt-0 mb-0 aspect-square h-full max-h-[90svh] w-full object-cover"
|
||||
>
|
||||
<source src={video2023_01_08} type="video/webm" />
|
||||
</video>
|
||||
<video
|
||||
preload="metadata"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
class="mt-0 mb-0 aspect-square h-full max-h-[90svh] w-full object-cover"
|
||||
>
|
||||
<source src={video2023_07_19} type="video/webm" />
|
||||
</video>
|
||||
</div>
|
||||
|
||||
import image2024_04_01 from "./2024-04-01.jpg";
|
||||
import image2022_01_27 from "./2022-01-27.jpg";
|
||||
import image2022_01_06 from "./2022-01-06.jpg";
|
||||
import image2023_11_23 from "./2023-11-23.jpg";
|
||||
import image2024_02_15 from "./2024-02-15.jpg";
|
||||
import image2023_10_12 from "./2023-10-12.jpg";
|
||||
import image2022_09_26 from "./2022-09-26.jpg";
|
||||
import image2023_11_02 from "./2023-11-02.jpg";
|
||||
import image2022_03_27 from "./2022-03-27.png";
|
||||
import image2022_05_17 from "./2022-05-17.jpg";
|
||||
import image2023_01_05 from "./2023-01-05.jpg";
|
||||
import image2024_07_19 from "./2024-07-19.jpg";
|
||||
import image2025_01_24 from "./2025-01-24.jpg";
|
||||
import image2022_03_28 from "./2022-03-28.jpg";
|
||||
import image2023_044_18 from "./2023-04-18.avif";
|
||||
import imageforestfire from "./forestfire.jpg";
|
||||
import imagedatsun from "./datsun.jpg";
|
||||
import imageedit from "./edit.jpg";
|
||||
import imagezomax from "./zomax.jpg";
|
||||
import imagesea from "./sea.jpg";
|
||||
import imagelovesongs from "./lovesongs-2-2153-P.jpg";
|
||||
|
||||
<Gallery
|
||||
items={[
|
||||
{ src: image2024_04_01, alt: "224 Torquay Road" },
|
||||
{ src: image2022_01_27, alt: "Studying Spider" },
|
||||
{ src: image2022_01_06, alt: "Firespline" },
|
||||
{ src: image2023_11_23, alt: "Ugolino and His Sons lighting" },
|
||||
{ src: image2024_02_15, alt: "Austin" },
|
||||
{ src: image2023_10_12, alt: "Austin" },
|
||||
{ src: image2022_09_26, alt: "Wes Cockx" },
|
||||
{ src: image2023_11_02, alt: "Trees" },
|
||||
{ src: image2022_03_27, alt: "Sewer" },
|
||||
{ src: image2022_05_17, alt: "Hanging light fixtures" },
|
||||
{ src: image2023_01_05, alt: "Digital Artefact: Corridor" },
|
||||
{ src: image2024_07_19, alt: "DOF tiles" },
|
||||
{ src: imageforestfire, alt: "Forest fire" },
|
||||
{ src: image2025_01_24, alt: "Escalator" },
|
||||
{ src: image2022_03_28, alt: "Astronaut" },
|
||||
{ src: imagedatsun, alt: "Datsun" },
|
||||
{ src: imagezomax, alt: "Zomax" },
|
||||
{ src: imagesea, alt: "Sea" },
|
||||
{ src: imagelovesongs, alt: "Love Songs record cover" },
|
||||
{ src: image2023_044_18, alt: "Abstract swirl" },
|
||||
{ src: imageedit, alt: "Little Nightmares" },
|
||||
]}
|
||||
/>
|
BIN
src/content/projects/archive/lovesongs-2-2153-P.jpg
Normal file
After Width: | Height: | Size: 805 KiB |
BIN
src/content/projects/archive/sea.jpg
Normal file
After Width: | Height: | Size: 545 KiB |
BIN
src/content/projects/archive/zomax.jpg
Normal file
After Width: | Height: | Size: 67 KiB |
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
title: "Astronaut"
|
||||
description: "Lighting and camera test."
|
||||
date: 2022-03-28
|
||||
updated: 2022-03-28
|
||||
image: { url: "troy-lusty-astronaut.avif", alt: "Astronaut final piece" }
|
||||
tags: ["blender", "davinci resolve"]
|
||||
categories: ["personal"]
|
||||
---
|
||||
|
||||
Astronaut character lit with 1 area light using light nodes. Done to test lighting and the creation of a camera with an "anamorphic lens" inside of Blender. Final adjustments made within DaVinci Resolve including adding a lens dirt overlay by William Landgren.
|
||||
|
||||

|
||||
|
||||
_Astronaut Final Image_
|
||||
|
||||
### External links
|
||||
|
||||
[Domenico D’Alisa’s ArtStation](https://www.artstation.com/domenicodalisa)
|
||||
|
||||
[William Landgren’s Instagram](https://www.instagram.com/landgrenwilliam)
|
||||
|
||||
[Astronaut asset](https://cubebrush.co/domenicodalisa/products/vsuspw/discovery-pay-what-you-want-2017)
|
Before Width: | Height: | Size: 11 KiB |
BIN
src/content/projects/camouflage-store/K7wlm60rXVs-HD.jpg
Normal file
After Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 33 KiB |
|
@ -6,12 +6,13 @@ date: 2022-10-18 # Date Persuit theme was purchased
|
|||
updated: 2024-12-05
|
||||
image:
|
||||
{
|
||||
url: "camouflage-store-barningham-raby.avif",
|
||||
url: "t_pkD9Wf0QU-HD.jpg",
|
||||
alt: "Camouflage Store YouTube video regarding Altberg boots comparison.",
|
||||
}
|
||||
tags: ["ecommerce", "shopify", "docker"]
|
||||
categories: ["client work"]
|
||||
featured: true
|
||||
rank: 1
|
||||
---
|
||||
|
||||
My role has me in charge of managing an [ecommerce store](https://camouflagestore.uk/) in addition to creating, editing, and publishing informational YouTube and social media content for a family run outdoors store. This includes the redesign shown below but also any maintenance and general upkeep of the site and all related systems.
|
||||
|
@ -20,7 +21,7 @@ My role has me in charge of managing an [ecommerce store](https://camouflagestor
|
|||
|
||||
As of 2024-12-05 the [YouTube channel](https://www.youtube.com/@camouflagestoreuk) has 1.37k subscribers and 312,241 views over a total of 168 videos. If were to pick one video that displays the quality of the content we produce, it would probably be [SOLO ATP SAS SMOCK MK2 (2022) OVERVIEW | Camouflage Store](https://www.youtube.com/watch?v=K7wlm60rXVs). I am incredibly grateful to Steve for giving me the opportunity to continue working with him, and for the amount of creative freedom he gives me when experimenting with new ideas.
|
||||
|
||||

|
||||

|
||||
|
||||
## Site redesign
|
||||
|
||||
|
@ -53,10 +54,3 @@ For the domain, we have gone with [camouflagestore.uk](https://camouflagestore.u
|
|||
Continuing my goal of giving Steve the most amount of freedom possible without having to rely on thirdparty services, I have setup a VPS on his behalf to host a variety of services.
|
||||
|
||||
The first of which is an instance of [Umami](https://umami.is/), a self-hostable analytics platform. This has been hosted using Docker and includes automatic redeployments using Watchtower, and reverse proxying with Traefik.
|
||||
|
||||
### Other links
|
||||
|
||||
- [Camouflage Store](https://camouflagestore.uk)
|
||||
- [YouTube](https://www.youtube.com/@camouflagestoreuk)
|
||||
- [Instagram](https://www.instagram.com/camouflagestoreuk)
|
||||
- [Twitter](https://twitter.com/camouflagestore)
|
||||
|
|
BIN
src/content/projects/camouflage-store/t_pkD9Wf0QU-HD.jpg
Normal file
After Width: | Height: | Size: 120 KiB |
|
@ -1,53 +0,0 @@
|
|||
---
|
||||
title: "Digital Artefact: Corridor (Incomplete)"
|
||||
description: "A virtual production horror environment made in Unreal Engine 5 and inspired by The Shining."
|
||||
date: 2023-01-20
|
||||
updated: 2023-01-20
|
||||
image:
|
||||
{
|
||||
url: "troy-lusty-highresscreenshot05012023-2.avif",
|
||||
alt: "Progress 5 for Digital Artefact: Corridor project",
|
||||
}
|
||||
tags: ["unreal engine", "blender", "davinci resolve", "photoshop"]
|
||||
categories: ["education"]
|
||||
includeHero: true
|
||||
---
|
||||
|
||||
import deltakey from "deltakey.webm";
|
||||
import wallpaperpeel from "wallpaperpeel.webm";
|
||||
|
||||
The outcome I went into this project expecting was that I would produce an environment made entirely from scratch which I could create a short test virtual production shot in utilising a motion capture camera rig and live keying. Later I would then properly composite the two bits of footage together.
|
||||
|
||||
**This project is presented here in the state it was upon the university deadline. There were a couple issues that occurred towards the end of production which is why the project is listed as incomplete.**
|
||||
|
||||

|
||||
|
||||
_Using Lumen for global illumination._
|
||||
|
||||

|
||||
|
||||
_Set the project to use deprecated hardware raytracing instead of Lumen, which resulted in the light spill on the walls being fixed._
|
||||
|
||||

|
||||
|
||||
_First steps of migrating the scene over to Unreal Engine. Focusing on recreating the lights from Blender into Lumen. I am having issues softening up the shadows being cast from the light shade and wall mounting._
|
||||
|
||||

|
||||
|
||||
_Modelled a new scene design by creating repeatable assets in Blender and utilising collection instancing._
|
||||
|
||||

|
||||
|
||||
_Initial idea made and presented in Blender for the project pitch._
|
||||
|
||||
<video preload="metadata" loop muted controls>
|
||||
<source src={deltakey} type="video/webm" />
|
||||
</video>
|
||||
|
||||
_Virtual production DaVinci Resolve delta key_
|
||||
|
||||
<video preload="metadata" loop muted controls>
|
||||
<source src={wallpaperpeel} type="video/webm" />
|
||||
</video>
|
||||
|
||||
_Wallpaper peel test_
|
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 2.4 KiB |
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
title: "Discord Bot"
|
||||
description: "AQA Computer Science NEA project based around creating a Discord bot."
|
||||
date: 2020-03-31
|
||||
updated: 2020-03-31
|
||||
image: { url: "discord.avif", alt: "Discord bot" }
|
||||
tags: ["python"]
|
||||
categories: ["education"]
|
||||
---
|
||||
|
||||
The objective I set myself was to write a Discord bot as my AQA Computer Science NEA Project. The program utilised [discord.py](https://github.com/Rapptz/discord.py), a Discord API wrapper for use within Python. The resulting code for the bot can be viewed in my Git repo.
|
||||
|
||||
### External links
|
||||
|
||||
https://code.troylusty.com/troy/discordbot
|
|
@ -1,28 +0,0 @@
|
|||
---
|
||||
title: "Firespline"
|
||||
description: "A fire animation test presented in a small cave scene."
|
||||
date: 2022-01-06
|
||||
updated: 2022-01-06
|
||||
image: { url: "troy-lusty-firespline.avif", alt: "Firespline frame" }
|
||||
tags: ["blender", "davinci resolve"]
|
||||
categories: ["personal"]
|
||||
---
|
||||
|
||||
import firespline from "firespline.webm";
|
||||
|
||||
Cycles and DaVinci Resolve (with assets from Blend Swap and ambientCG)
|
||||
This is fire animation test I did which I turned into a scene complete with reflected light on water and volumetrics.
|
||||
|
||||
<video preload="metadata" loop muted controls>
|
||||
<source src={firespline} type="video/webm" />
|
||||
</video>
|
||||
|
||||
_Animation_
|
||||
|
||||

|
||||
|
||||
_Extracted frame_
|
||||
|
||||
### Assets list
|
||||
|
||||
https://www.blendswap.com/blend/26395, https://ambientcg.com/view?id=Rock030, https://ambientcg.com/view?id=Ground033
|
Before Width: | Height: | Size: 9.3 KiB |
|
@ -1,40 +0,0 @@
|
|||
---
|
||||
title: "Kikimora"
|
||||
description: "A narrative driven horror game prototype done as a university project."
|
||||
date: 2024-01-24
|
||||
image: { url: "kikimora-titlecard.avif", alt: "Kikimora titlecard" }
|
||||
tags: ["godot", "blender", "gimp", "inkscape"]
|
||||
categories: ["education"]
|
||||
---
|
||||
|
||||
import kikimora_gameplay from "kikimora-gameplay.webm";
|
||||
|
||||
This was my first attempt at making anything within the Godot game engine, and was consequently what resulted in me starting the creation of [MUST FIND BEANS](https://store.steampowered.com/app/3012740/MUST_FIND_BEANS/).
|
||||
|
||||
### Controls
|
||||
|
||||
WASD - Movement
|
||||
|
||||
CTRL - Crouch
|
||||
|
||||
E - Interact
|
||||
|
||||
Mouse - Look around
|
||||
|
||||
<video preload="metadata" controls>
|
||||
<source src={kikimora_gameplay} type="video/webm" />
|
||||
</video>
|
||||
|
||||
_Short gameplay video_
|
||||
|
||||

|
||||
|
||||
_Screenshot 1_
|
||||
|
||||

|
||||
|
||||
_Screenshot 2_
|
||||
|
||||
### External links
|
||||
|
||||
[Itch.io page](https://troylusty.itch.io/kikimora)
|