first commit

This commit is contained in:
Troy 2024-12-23 21:18:55 +00:00
commit ff7c974867
Signed by: troy
GPG key ID: DFC06C02ED3B4711
227 changed files with 12908 additions and 0 deletions

15
.dockerignore Normal file
View file

@ -0,0 +1,15 @@
node_modules
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
Makefile
helm-charts
.env
.editorconfig
.idea
coverage*

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto

1
.github/CODEOWNERS vendored Normal file
View file

@ -0,0 +1 @@
* @troylusty

6
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"

53
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,53 @@
name: Docker
on:
push:
branches:
- "main"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
permissions:
packages: write
contents: read
jobs:
run-tests:
uses: ./.github/workflows/test.yml
build-and-push-image:
runs-on: ubuntu-latest
needs:
- run-tests
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Get repository name
run: |
echo "REPO_NAME=${GITHUB_REPOSITORY#$GITHUB_REPOSITORY_OWNER/}" >> $GITHUB_ENV
- name: Delete oldest packages
uses: actions/delete-package-versions@v5
with:
package-name: ${{ env.REPO_NAME }}
package-type: "container"
min-versions-to-keep: 25

23
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: Test
on:
pull_request:
workflow_call:
permissions:
contents: read
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "latest"
- name: Run Node install and build
run: |
npm ci
npm run build

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

11
.prettierrc.json Normal file
View file

@ -0,0 +1,11 @@
{
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}

12
Dockerfile Normal file
View file

@ -0,0 +1,12 @@
FROM node:alpine as node
USER node
WORKDIR /usr/src/app
COPY . .
RUN ["npm", "ci"]
RUN ["npm", "run", "build"]
FROM ghcr.io/static-web-server/static-web-server:latest
WORKDIR /
COPY --from=node /usr/src/app/dist /public

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Troy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

60
README.md Normal file
View file

@ -0,0 +1,60 @@
# Astro Portfolio: Personal Website
![showcase](https://github.com/user-attachments/assets/782dd9e5-7f90-4091-8fa4-2c078111a93c)
Features:
- ✅ SEO-friendly
- ✅ Sitemap
- ✅ RSS Feed
- ✅ Markdown & MDX
- ✅ TailwindCSS
- ✅ Fontsource
## 🚀 Project Structure
Inside of this Astro project, you'll see the following folders and files:
```text
├── public/
├── src/
│ ├── components/
│ ├── content/
│ ├── layouts/
│ └── pages/
├── Dockerfile
├── README.md
├── astro.config.ts
├── package.json
├── tailwind.config.ts
└── tsconfig.json
```
The layout of directories and content should match Astro's own recommendations with components being found in `src/components/` for example.
Project and post articles are contained within MDX documents located in `src/content/`. This has been done to allow for videos to be embedded when they are also kept in the corresponding content directory.
## 🚧 Building
Docker is used to deploy the site to a VPS. Container images are built using a [GitHub Action](.github/workflows/docker.yml) from the included [Dockerfile](Dockerfile).
## 🧞 Commands
All commands are run from the root of the project, from a terminal.
| Command | Action |
| :------------------------ | :------------------------------------------------------ |
| `npm install` | Install dependencies |
| `npm run dev` | Start local dev server at `localhost:4321` |
| `npm run build` | Build production site to `./dist/` |
| `npm run preview` | Preview build locally, before deploying |
| `npm run format:check` | Check files with Prettier |
| `npm run format:write` | Run Prettier on all files, rewriting all files in place |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
All available commands can be found by running `npm run` from a terminal.
## 📜 Licence
This project is under the [MIT LICENSE](LICENSE). However, this applies to the **ONLY** to the website itself and does not extend to the artwork included within.

37
astro.config.ts Normal file
View file

@ -0,0 +1,37 @@
import { defineConfig, passthroughImageService } from "astro/config";
import sitemap from "@astrojs/sitemap";
import rehypeExternalLinks from "rehype-external-links";
import mdx from "@astrojs/mdx";
import tailwind from "@astrojs/tailwind";
import icon from "astro-icon";
// https://astro.build/config
export default defineConfig({
site: "https://troylusty.com",
integrations: [sitemap(), mdx(), tailwind(), icon()],
output: "static",
markdown: {
rehypePlugins: [
[
rehypeExternalLinks,
{
target: "_blank",
rel: "noopener nofollow noreferrer",
},
],
],
shikiConfig: {
wrap: true,
},
syntaxHighlight: false,
},
image: {
service: passthroughImageService(),
},
build: {
inlineStylesheets: "never",
},
experimental: {
responsiveImages: true,
},
});

9951
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

37
package.json Normal file
View file

@ -0,0 +1,37 @@
{
"name": "astro",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"format:check": "prettier --check .",
"format:write": "prettier --write ."
},
"dependencies": {
"@astrojs/check": "0.9.4",
"@astrojs/mdx": "^4.0.3",
"@astrojs/rss": "^4.0.10",
"@astrojs/sitemap": "3.2.1",
"@astrojs/tailwind": "^5.1.4",
"@fontsource-variable/inter": "^5.1.0",
"@fontsource-variable/red-hat-mono": "^5.1.0",
"astro": "^5.1.1",
"astro-icon": "^1.1.4",
"rehype-external-links": "^3.0.0",
"tailwindcss": "^3.4.15",
"typescript": "^5.7.2"
},
"devDependencies": {
"@iconify-json/mdi": "^1.2.1",
"@iconify-json/simple-icons": "^1.2.13",
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.10.1",
"prettier": "^3.4.1",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.9"
}
}

View file

@ -0,0 +1,4 @@
Contact: mailto:security@troylusty.com
Expires: 2025-01-01T00:00:00.000Z
Encryption: https://troylusty.com/gpg.txt
Preferred-Languages: en

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/assets/gradient.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

9
public/favicon.svg Normal file
View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M 31.999999,0 A 32,31.999999 0 0 0 0,31.999999 V 95.999997 A 32,31.999999 0 0 0 31.999999,128 h 64 A 32,31.999999 0 0 0 128,95.999997 V 31.999999 A 32,31.999999 0 0 0 95.999999,0 Z m 9.813477,32.85327 H 86.186522 V 44.373292 H 70.399903 V 95.146726 H 57.600098 V 44.373292 H 41.813476 Z" />
<style>
path { fill: black; }
@media (prefers-color-scheme: dark) {
path { fill: white; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 519 B

13
public/gpg.txt Normal file
View file

@ -0,0 +1,13 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEZdVLqRYJKwYBBAHaRw8BAQdA/unLkO7qkDQkmJ7q3ixLg92NymjFpjG6Jy6L
KbhI+UO0GlRyb3kgPGhlbGxvQHRyb3lsdXN0eS5jb20+iJMEExYKADsWIQRqhsiE
rQZiEhIMJ33fwGwC7TtHEQUCZdVLqQIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe
BwIXgAAKCRDfwGwC7TtHEbyPAQC0P2OixyoWOX/7A7tHhKWJBWosmhGJ+OVfrA4t
ACniWwD/ViSbq7IkPKCP7q92iVwP5eYr2SW65qb/vYPTWQCIKQ24OARl1UupEgor
BgEEAZdVAQUBAQdA3blqr6MQuI/h1L0Qs+VdXrkOFC59uYh3M1E2mD2h7XcDAQgH
iHgEGBYKACAWIQRqhsiErQZiEhIMJ33fwGwC7TtHEQUCZdVLqQIbDAAKCRDfwGwC
7TtHEXuJAPwILQFV92bYGiTidgNwRTnjpk6UsyLRqiUy7XQ1eG3JRQD7BqtYPD1U
9DyDtkTjydWQJzoFfokMY3VwUAn/+2KZdAQ=
=Pxdb
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -0,0 +1,35 @@
---
import { Icon } from "astro-icon/components";
type Props = {
institution: String;
qualification: String;
grades: Array<String>;
isOpen?: boolean;
};
const { institution, qualification, grades, isOpen = false } = Astro.props;
---
<div class="grid">
<details open={isOpen === true ? "open" : null} class="group">
<summary
class="flex cursor-pointer items-center justify-between py-3 font-bold"
>
<p class="m-0">
{institution}
</p>
<span class="transition group-open:rotate-180">
<Icon name="mdi:chevron-down" class="h-6 w-auto text-tertiary" />
</span>
</summary>
<div class="p-4 text-sm text-neutral-600 dark:text-neutral-400">
<p class="my-0">
{qualification}
</p>
<ul>
{grades.map((grade) => <li>{grade}</li>)}
</ul>
</div>
</details>
</div>

View file

@ -0,0 +1,106 @@
---
import Layout from "@layouts/Layout.astro";
import Prose from "@components/Prose.astro";
import FormattedDate from "@components/FormattedDate.astro";
import { readingTime } from "@lib/utils";
import { Icon } from "astro-icon/components";
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;
}
const listFormatter = new Intl.ListFormat("en-GB", {
style: "long",
type: "conjunction",
});
---
<Layout
title={article.data.title}
description={article.data.description}
image={article.data.image.url.src}
date={article.data.date}
updated={article.data.updated}
tags={article.data.tags}
>
<div class="mx-auto mb-16 max-w-prose">
<h1
class="animate-reveal break-words text-start text-4xl font-medium opacity-0"
>
{article.data.title}
</h1>
<div
class="flex animate-reveal flex-col items-start opacity-0 [animation-delay:0.3s]"
>
<div
class="mt-4 flex flex-col items-start gap-2 text-lg text-tertiary md:flex-row"
>
<div class="flex items-center gap-2">
<Icon name="mdi:calendar" />
{
datesMatch ? (
<p title="Date">
<FormattedDate date={article.data.date} />
</p>
) : (
<>
<p title="Date">
<FormattedDate date={article.data.date} />
</p>
<Icon name="mdi:trending-up" />
<p title="Updated">
<FormattedDate date={article.data.updated} />
</p>
</>
)
}
</div>
{
isPost ? (
<div class="flex items-center gap-2">
<Icon name="mdi:timer" />
<p title="Word count">{readingTime(article.body)}</p>
</div>
) : null
}
</div>
{
article.data.extraAuthors ? (
<div class="mt-2 flex items-center gap-2 text-tertiary">
<p>
In collaboration with{" "}
{listFormatter.format(article.data.extraAuthors)}
</p>
</div>
) : null
}
<ul class="mt-4 flex flex-wrap gap-1">
{
article.data.categories.map((category: string) => (
<li class="rounded border border-accent bg-accent/50 px-1 py-0.5 text-sm capitalize text-primary invert">
{category}
</li>
))
}
{
article.data.tags.map((tag: string) => (
<li class="rounded border border-accent bg-accent/50 px-1 py-0.5 text-sm capitalize text-secondary">
{tag}
</li>
))
}
</ul>
</div>
</div>
<div
class="mx-auto max-w-prose animate-reveal opacity-0 [animation-delay:0.6s]"
>
<Prose>
<Content />
</Prose>
</div>
</Layout>

View file

@ -0,0 +1,30 @@
---
import { getCollection } from "astro:content";
import { dateRange } from "@lib/utils";
import Accordion from "@components/Accordion.astro";
const collection = (await getCollection("education")).sort(
(a, b) =>
new Date(b.data.dateStart).valueOf() - new Date(a.data.dateStart).valueOf(),
);
const education = await Promise.all(
collection.map(async (item) => {
const { Content } = await item.render();
return { ...item, Content };
}),
);
---
<div>
{
education.map((entry) => (
<Accordion
institution={`${entry.data.institution} (${dateRange(entry.data.dateStart, entry.data.dateEnd)})`}
qualification={entry.data.qualification}
grades={entry.data.grades}
isOpen={entry.data.isOpen}
/>
))
}
</div>

View file

@ -0,0 +1,64 @@
---
import { SITE } from "@consts";
import Link from "@components/Link.astro";
import { Icon } from "astro-icon/components";
---
<footer class="mt-auto">
<div class="mx-auto w-full max-w-screen-lg p-4 py-6 lg:py-8">
<div class="md:flex md:justify-between">
<div class="mb-6 text-secondary md:mb-0">
<a class="inline-flex items-center" href="#top" title="Back to top">
<Icon name="icon" title={SITE.TITLE} class="h-8 w-auto ease-in-out" />
<div
class="ml-2 hidden flex-none text-sm font-bold capitalize md:visible lg:block"
>
Troy Lusty
</div>
</a>
</div>
<div class="text-left sm:gap-6 md:text-right">
<div>
<h2 class="mb-6 text-sm font-semibold uppercase text-secondary">
Sections
</h2>
<ul class="font-medium text-tertiary">
{
SITE.NAVLINKS.map((i) => (
<li class="mb-4 last:mb-0">
<a
data-navlink
href={i.href}
class="capitalize hover:text-secondary"
>
{i.name}
</a>
</li>
))
}
</ul>
</div>
</div>
</div>
<div class="mt-12 sm:flex sm:items-center sm:justify-between lg:mt-16">
<span class="text-sm text-tertiary sm:text-center"
>&copy; {new Date().getFullYear()}
<a href="/" class="hover:text-secondary">{SITE.TITLE}</a>. All Rights
Reserved.
</span>
<div class="mt-4 flex gap-5 sm:mt-0 sm:justify-center">
{
SITE.LINKS.map((i) => (
<Link href={i.href}>
<Icon
name={i.icon}
title={i.name}
class="h-5 w-5 text-tertiary hover:text-secondary"
/>
</Link>
))
}
</div>
</div>
</div>
</footer>

View file

@ -0,0 +1,17 @@
---
interface Props {
date?: Date;
}
const { date = new Date() } = Astro.props;
---
<time datetime={date.toISOString()}>
{
date.toLocaleDateString("en-GB", {
day: "2-digit",
month: "short",
year: "numeric",
})
}
</time>

121
src/components/Head.astro Normal file
View file

@ -0,0 +1,121 @@
---
import { SITE } from "@consts";
import gradient from "../../public/assets/gradient.avif";
interface Props {
title: string;
description: string;
image?: string;
date?: Date;
updated?: Date;
tags?: Array<string>;
}
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image = gradient.src, date, updated } = Astro.props;
let { tags } = Astro.props;
if (typeof tags !== "undefined") {
tags = SITE.KEYWORDS.concat(tags);
}
import inter from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url";
import redhatmono from "@fontsource-variable/red-hat-mono/files/red-hat-mono-latin-wght-normal.woff2?url";
---
<head>
<!-- Global Metadata -->
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta content="True" name="HandheldFriendly" />
<meta content="en-gb" name="lang" />
<!-- Favicon -->
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/favicon.ico" sizes="32x32" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<!-- Generator -->
<meta name="generator" content={Astro.generator} />
<!-- Author -->
<meta content={SITE.AUTHOR} name="author" />
<!-- Sitemap -->
<link rel="sitemap" href="/sitemap-index.xml" />
<!-- RSS -->
<link
rel="alternate"
type="application/rss+xml"
title={SITE.TITLE}
href="/rss.xml"
}
/>
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<!-- Keywords -->
<meta
content={tags ? tags?.toString() : SITE.KEYWORDS.toString()}
name="keywords"
/>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.url)} />
<meta property="og:site_name" content={SITE.TITLE} />
{
date ? (
<meta property="article:published_time" content={date.toISOString()} />
) : null
}
{
updated ? (
<meta property="article:modified_time" content={updated.toISOString()} />
) : null
}
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />
<!-- View Transitions -->
<style>
@view-transition {
navigation: auto;
}
</style>
<!-- Disable Dark Reader Statically -->
<meta name="darkreader-lock" />
<!-- Font Preload -->
<link
rel="preload"
as="font"
type="font/woff2"
href={inter}
crossorigin="anonymous"
/>
<link
rel="preload"
as="font"
type="font/woff2"
href={redhatmono}
crossorigin="anonymous"
/>
</head>

View file

@ -0,0 +1,19 @@
---
import { SITE } from "@consts";
import { Icon } from "astro-icon/components";
---
<header id="header" class="mx-auto w-full max-w-screen-lg p-4">
<div
class="flex h-12 items-center justify-between leading-[0px] text-secondary"
>
<a class="inline-flex items-center" href="/" title={SITE.TITLE}>
<Icon name="icon" title={SITE.TITLE} class="h-8 w-auto ease-in-out" />
<div
class="ml-2 hidden flex-none text-sm font-bold capitalize md:visible lg:block"
>
Troy Lusty
</div>
</a>
</div>
</header>

20
src/components/Link.astro Normal file
View file

@ -0,0 +1,20 @@
---
import type { HTMLAttributes } from "astro/types";
interface Props extends HTMLAttributes<"a"> {
href: string;
external?: boolean;
class?: string;
}
const { href, external = true, ...rest } = Astro.props;
---
<a
href={href}
rel={external ? "noopener nofollow noreferrer" : ""}
target={external ? "_blank" : "_self"}
{...rest}
>
<slot />
</a>

View file

@ -0,0 +1,5 @@
<div
class="prose max-w-full prose-headings:text-secondary prose-h1:text-xl prose-h1:font-bold prose-p:max-w-full prose-p:text-pretty prose-p:break-words prose-p:text-lg prose-p:text-tertiary prose-a:text-secondary prose-a:underline prose-a:decoration-tertiary/30 prose-a:decoration-wavy prose-blockquote:border-secondary prose-strong:text-secondary prose-code:whitespace-pre-wrap prose-code:font-semibold prose-code:text-tertiary prose-code:before:content-none prose-code:after:content-none prose-pre:w-fit prose-pre:max-w-full prose-pre:border prose-pre:border-accent prose-pre:bg-accent/50 prose-pre:text-tertiary prose-li:text-tertiary prose-li:marker:text-secondary prose-img:max-h-[90vh] prose-img:w-auto prose-img:max-w-full prose-img:rounded prose-video:max-h-[95vh] prose-video:w-auto prose-video:max-w-full prose-video:rounded"
>
<slot />
</div>

View file

@ -0,0 +1,49 @@
---
import { Image } from "astro:assets";
import FormattedDate from "@components/FormattedDate.astro";
type Props = {
collection: any;
};
const { collection } = Astro.props;
---
<article
class="group relative isolate mx-auto flex w-full flex-col justify-end overflow-hidden rounded-lg px-8 pb-8 pt-40"
>
<Image
src={collection.data.image.url}
alt={collection.data.image.alt}
title={collection.data.title}
loading="eager"
class="absolute inset-0 h-full w-full object-cover duration-300 ease-in-out group-hover:scale-105"
fit="cover"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-black via-transparent to-transparent"
>
</div>
<a
class="absolute inset-0 z-20"
href={`/${collection.collection}/${collection.slug}`}
aria-label={collection.data.title}></a>
<h3
class="z-10 mt-3 w-fit text-xl font-medium text-primary dark:text-secondary"
>
{collection.data.title}
</h3>
<div
class="z-10 w-fit gap-y-1 overflow-hidden text-sm leading-6 text-tertiary"
>
{
collection.data.collection ? (
<span>
<FormattedDate date={collection.data.date} /> &bull; Collection
</span>
) : (
<FormattedDate date={collection.data.date} />
)
}
</div>
</article>

View file

@ -0,0 +1,23 @@
---
import Layout from "@layouts/Layout.astro";
import { SITE } from "@consts";
import Showcase from "@components/Showcase.astro";
interface Props {
content: any;
CONSTS: any;
}
const { content, CONSTS } = Astro.props;
---
<Layout title={SITE.TITLE} description={CONSTS.DESCRIPTION}>
<h1 class="animate-reveal break-words text-4xl font-medium opacity-0">
{CONSTS.TITLE}
</h1>
<div
class="mt-16 grid animate-reveal grid-cols-1 gap-6 opacity-0 [animation-delay:0.4s] md:grid-cols-3 md:[&>*:nth-child(4n+2)]:col-span-2 md:[&>*:nth-child(4n+3)]:col-span-2 md:[&>*:only-child]:col-span-3"
>
{content.map((article: any) => <Showcase collection={article} />)}
</div>
</Layout>

View file

@ -0,0 +1,27 @@
---
import { getCollection } from "astro:content";
import { Icon } from "astro-icon/components";
const collection = await getCollection("skills");
const skills = await Promise.all(
collection.map(async (item) => {
const { Content } = await item.render();
return { ...item, Content };
}),
);
---
<ul class="flex max-w-full list-none flex-wrap gap-4 px-0">
{
skills.map((entry) => (
<li>
<Icon
name={entry.data.icon}
title={entry.data.title}
class="h-12 w-auto text-secondary"
/>
</li>
))
}
</ul>

View file

@ -0,0 +1,5 @@
<div
class="mx-auto mb-8 mt-2 max-w-full p-4 pb-16 md:mb-32 md:mt-16 md:max-w-screen-lg md:p-5"
>
<slot />
</div>

36
src/components/Work.astro Normal file
View file

@ -0,0 +1,36 @@
---
import { getCollection } from "astro:content";
import { dateRange } from "@lib/utils";
const collection = (await getCollection("work")).sort(
(a, b) =>
new Date(b.data.dateStart).valueOf() - new Date(a.data.dateStart).valueOf(),
);
const work = await Promise.all(
collection.map(async (item) => {
const { Content } = await item.render();
return { ...item, Content };
}),
);
---
<ul class="list-none pl-0">
{
work.map((entry) => (
<li class="pl-0">
<h3>
<span>{entry.data.company}</span>
<span>({dateRange(entry.data.dateStart, entry.data.dateEnd)})</span>
</h3>
<p>{entry.data.role}</p>
<article>
<entry.Content />
</article>
{entry.data.article ? (
<a href={entry.data.article}>See related project</a>
) : null}
</li>
))
}
</ul>

102
src/consts.ts Normal file
View file

@ -0,0 +1,102 @@
import type { Metadata, Site } from "@types";
export const SITE: Site = {
TITLE: "Troy Lusty",
DESCRIPTION:
"Hi, my name is Troy and Im a student 3D artist studying on a BA (Hons) Game Arts and Design course in the UK.",
EMAIL: "hello@troylusty.com",
KEYWORDS: [
"troy",
"lusty",
"troylusty",
"portfolio",
"3d",
"design",
"graphics",
"blender",
"photoshop",
"davinci",
"resolve",
"unreal",
"engine",
"godot",
"games",
],
AUTHOR: "Troy Lusty",
LINKS: [
{
name: "RSS feed",
href: "/rss.xml",
icon: "mdi:rss",
},
{
name: "Sitemap",
href: "/sitemap-index.xml",
icon: "mdi:sitemap",
},
{
name: "Email",
href: "mailto:hello@troylusty.com",
icon: "mdi:email",
},
{
name: "GitHub",
href: "https://github.com/troylusty",
icon: "mdi:github",
},
{
name: "Steam developer",
href: "https://store.steampowered.com/developer/troy",
icon: "mdi:steam",
},
],
NAVLINKS: [
{
name: "home",
href: "/",
},
{
name: "projects",
href: "/projects",
},
{
name: "posts",
href: "/posts",
},
{
name: "curriculum vitae",
href: "/cv",
},
],
};
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 = {
TITLE: "Projects",
DESCRIPTION:
"A collection of my projects, with links to repositories and demos.",
};

76
src/content/config.ts Normal file
View file

@ -0,0 +1,76 @@
import { defineCollection, z } from "astro:content";
const posts = defineCollection({
type: "content",
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
updated: z.date().optional(),
draft: z.boolean().optional(),
image: z.object({
url: image(),
alt: z.string(),
}),
tags: z.array(z.string()),
extraAuthors: z.array(z.string()).optional(),
categories: z.array(z.string()),
}),
});
const projects = defineCollection({
type: "content",
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
updated: z.date().optional(),
draft: z.boolean().optional(),
image: z.object({
url: image(),
alt: z.string(),
}),
tags: z.array(z.string()),
extraAuthors: z.array(z.string()).optional(),
categories: z.array(z.string()),
featured: z.boolean().optional(),
collection: z.boolean().optional(),
includeHero: z.boolean().optional(),
}),
});
const work = defineCollection({
type: "content",
schema: z.object({
company: z.string(),
role: z.string(),
dateStart: z.coerce.date(),
dateEnd: z.union([z.coerce.date(), z.string()]),
article: z.string().url().optional(),
}),
});
const education = defineCollection({
type: "content",
schema: z.object({
institution: z.string(),
qualification: z.string(),
grades: z.array(z.string()),
dateStart: z.coerce.date(),
dateEnd: z.union([z.coerce.date(), z.string()]),
isOpen: z.boolean().optional(),
}),
});
const skills = defineCollection({
type: "content",
schema: z.object({
type: z.string(),
title: z.string(),
icon: z.string(),
}),
});
export const collections = { posts, projects, work, education, skills };

View file

@ -0,0 +1,11 @@
---
institution: "Kennicott Sixth Form"
qualification: "BTEC & A-level"
grades:
[
"Pearson BTEC Level 3 National Extended Diploma in Art and Design - Distinction Merit Merit (2020)",
"AQA GCE/A Computer Science ADV (Python) - C (2020)",
]
dateStart: "2018"
dateEnd: "2020"
---

View file

@ -0,0 +1,11 @@
---
institution: "King Edward VI Community College"
qualification: "GCSEs & Cambridge Nationals qualification"
grades:
[
"10 GCSEs including Maths and English (2018)",
"OCR Cambridge Nationals Creative iMedia Level 1/2 Award/Certificate - Merit at Level 2 (2016)",
]
dateStart: "2014"
dateEnd: "2018"
---

View file

@ -0,0 +1,11 @@
---
institution: "South Devon College"
qualification: "UAL Level 3 Extended Diploma in Creative Media Production and Technology"
grades:
[
"2nd year: Extended Diploma - Distinction (2022)",
"1st year: Diploma - Distinction (2021)",
]
dateStart: "2020"
dateEnd: "2022"
---

View file

@ -0,0 +1,12 @@
---
institution: "University Centre South Devon"
qualification: "FdA Games and Interactive Design"
grades:
[
"2nd year: 70.25% State Aggregate Mark (2024)",
"1st year: 69.43% State Aggregate Mark (2023)",
]
dateStart: "2022"
dateEnd: "2024"
isOpen: true
---

View file

@ -0,0 +1,7 @@
---
institution: "University of Plymouth"
qualification: "BA (Hons) Game Arts and Design"
grades: ["1st year: Uncommenced"]
dateStart: "2024"
dateEnd: "2025"
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View file

@ -0,0 +1,144 @@
---
title: "Libreboot on an X230"
date: 2024-01-07
updated: 2024-07-09
description: "An easy guide on how I flashed Libreboot onto my Lenovo Thinkpad X230, and later internally flashed an updated bios version."
image:
url: "flashing.avif"
alt: "Flashing the chips"
categories: ["personal"]
tags: ["linux", "libreboot", "raspberry pi", "thinkpad", "x230"]
---
This post is a simple guide on how I "Librebooted" my Lenovo Thinkpad X230. I struggled to find instructions which detailed the entire process for a beginner who has never used tools such as flashrom before. The examples I have given here are based on what I did personally. Whilst these are the exact steps I took, for you there may be other or completely different steps so please reference the [documentation](https://libreboot.org/docs/install/x230_external.html) before carrying out any steps listed here.
**If you are unsure of what size rom you require, you can find this out by reading the data from each chip and checking the resulting files.**
In my case I used a Raspberry Pi 3 Model B, a Pomona SOIC Clip 8 Pin, and a minimum of 6 female jumper wires.
![Flashing the chips](flashing.avif)
Before starting remember to remove the laptop's battery. Some guides state that you should also disconnect the CMOS battery however I didn't. When connecting and disconnecting the SOIC clip, ensure that the Pi is not connected to power to avoid damaging the chip.
## Preparing the rom
1. Download Libreboot's build system.
```sh
git clone https://codeberg.org/libreboot/lbmk
```
2. [Download](https://libreboot.org/download.html) the rom from Libreboot.
_stable > 20230625 (or the latest folder for you) > roms > libreboot-20230625_x230_12mb.tar.xz_
3. Extract the downloaded tar archive. Inside _bin > x230_12mb_ pick the correct rom for your keyboard layout and whether you want to use GRUB or SeaBIOS.
4. Run the below command **from inside** the lbmk directory and point it to the rom you chose above. Put your devices MAC address after the _-m_ switch as shown. Mine was printed on a sticker located inside the machine.
```sh
./vendor inject -r grub_x230_12mb_libgfxinit_corebootfb_ukqwerty.rom -b x230_12mb -m 00:f6:f0:40:71:fd
```
5. Split the rom for flashing onto the two chips.
```sh
dd if=grub_x230_12mb_libgfxinit_corebootfb_ukqwerty.rom of=top.rom bs=1M skip=8
dd if=grub_x230_12mb_libgfxinit_corebootfb_ukqwerty.rom of=bottom.rom bs=1M count=8
```
## Flashing
1. Read the top chip 2 or 3 times into separate files and compare the outcome using _diff_. This ensures that the data received has no discrepancies.
```sh
sudo flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=32768 -r factory_bios_top_01.rom -c "MX25L3206E/MX25L3208E" -V
```
2. Read bottom chip 2 or 3 times and again compare the outcome.
```sh
sudo flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=32768 -r factory_bios_bottom_01.rom -c "MX25L6406E/MX25L6408E" -V
```
3. Flash the top chip.
```sh
sudo flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=32768 -w top.rom -c "MX25L3206E/MX25L3208E" -V
```
4. Flash the bottom chip.
```sh
sudo flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=32768 -w bottom.rom -c "MX25L6406E/MX25L6408E" -V
```
### References
[Libreboot](https://libreboot.org/docs/install/x230_external.html), [Skulls](https://github.com/merge/skulls/blob/master/x230/README.md), [thinkpad-ec](https://github.com/hamishcoleman/thinkpad-ec/blob/master/docs/CONFIG.md), and [Harmonic Flow](https://www.harmonicflow.org/en/blog/2022/flashing-coreboot-on-a-lenovo-thinkpad-x230-with-a-raspberry-pi-tutorial).
## Internal flashing to update Libreboot
1. Find flash chip size.
```sh
flashprog -p internal
```
2. If flashprog tells you '/dev/mem mmap failed: Operation not permitted' then use the following command.
```sh
sudo modprobe -r lpc_ich
```
3. Install flashprog and optionally dmidecode from the AUR.
```sh
paru -S flashprog dmidecode
```
4. Read the current chip contents several times. To do this, run the same command changing the dump.bin filename.
```sh
sudo flashprog -p internal:laptop=force_I_want_a_brick,boardmismatch=force -r dump.bin
```
5. Clone Libreboot MaKe and change into the directory.
```sh
git clone https://codeberg.org/libreboot/lbmk; cd lbmk
```
6. Run the dependencies installer script. The content of which can be found in: `config/dependencies/arch`, if using Arch Linux for example.
```sh
sudo ./build dependencies arch
```
7. Manually install missing dependencies listed at the end of the script.
```sh
paru -S bdf-unifont ttf-unifont # ttf-unifont was meant to be unifont
```
8. Run the injection script to patch the release rom with the necessary vendor files.
```sh
./vendor inject -r seabios_withgrub_x230_12mb_libgfxinit_corebootfb_ukqwerty_grubfirst.rom -b x230_12mb -m 00:f6:f0:40:71:fd
```
9. Erase and rewrite the chip contents with the new rom.
```sh
sudo flashprog -p internal:laptop=force_I_want_a_brick,boardmismatch=force -w seabios_withgrub_x230_12mb_libgfxinit_corebootfb_ukqwerty_grubfirst.rom
```
10. Identify and clean-up installed dependencies.
```sh
paru -Qe
```
### References
[Flashprog FAQ](https://flashprog.org/wiki/FAQ), [Libreboot Build Dependencies](https://libreboot.org/docs/build/#first-install-build-dependencies), [Libreboot Internal Flashing](https://libreboot.org/docs/install/#run-flashprog-on-host-cpu).

View file

@ -0,0 +1,23 @@
---
title: "3D Package Design"
description: "3D Package Design inspired by 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_
![3D package design frame](troy-lusty-3d-package-design.avif)

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -0,0 +1,66 @@
---
title: "A Long Way Down (Demo)"
description: "A Long Way Down is a short, atmospheric linear adventure created alongside my friend and teammate Sam as a project for our 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" }
tags: ["unreal engine", "blender", "inkscape"]
categories: ["education"]
includeHero: true
extraAuthors: ["Sam Griffiths"]
---
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.
<video preload="metadata" controls>
<source src={alongwaydown_demo_walkthrough} type="video/webm" />
</video>
_Demo walkthrough_
![A Long Way Down Intro Showcase](alwd-img1.avif)
_Intro_
![A Long Way Down Forest Showcase](alwd-img2.avif)
_Forest_
![A Long Way Down Tree Bridge Showcase](alwd-img3.avif)
_Tree bridge_
![A Long Way Down Swamp Showcase](alwd-img4.avif)
_Swamp_
![A Long Way Down Final Climb](alwd-img5.avif)
_Final climb_
![A Long Way Down Cliff Jump](alwd-img6.avif)
_Cliff jump_
![Alternate night lighting idea](alwd-alternate-night-lighting.avif)
_Alternate night lighting idea_
![Old colour grade on forest section](alwd-early-forest.avif)
_Old colour grade on forest section_
![Early environment stage](alwd-early-environment-stage.avif)
_Early environment stage_
![Initial style experiments](alwd-style-experiment.avif)
_Initial style experiments_
### External links
[Itch.io page](https://samandtroy.itch.io/alongwaydown)

View file

@ -0,0 +1,23 @@
---
title: "Astronaut"
description: "Astronaut (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 piece](troy-lusty-astronaut.avif)
_Astronaut Final Image_
### External links
[Domenico D&rsquo;Alisa&rsquo;s ArtStation](https://www.artstation.com/domenicodalisa)
[William Landgren&rsquo;s Instagram](https://www.instagram.com/landgrenwilliam)
[Astronaut asset](https://cubebrush.co/domenicodalisa/products/vsuspw/discovery-pay-what-you-want-2017)

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -0,0 +1,61 @@
---
title: "Camouflage Store"
description: "Camouflage is a family run outdoor clothing and equipment business, which started in 2007. We also provide consultation to a major MOD Supplier. This includes product concept design and testing. With these partnerships we are able to provide the best possible products, designed, tested and proven."
#date: 2020-12-03T09:17:23+00:00 # Date of site creation
date: 2022-10-18 # Date Persuit theme was purchased
updated: 2024-12-05
image:
{
url: "camouflage-store-homepage.avif",
alt: "Camouflage Store homepage as of 2022",
}
tags: ["ecommerce", "shopify", "docker"]
categories: ["client work"]
---
My role has me in charge of managing an online ecommerce store 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.
## YouTube content
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.
![SOLO ATP SAS SMOCK MK2 (2022) OVERVIEW | Camouflage Store YouTube Video Thumbnail](camouflage-store-video-thumbnail.avif)
## Site redesign
Most recently I completed modernising the website which included moving it to Shopify from its previous CMS platform. This has resulted in the owner gaining more control over his business as he can now make changes to the site that would have previously required hiring a developer to do. Additionally, the new site is far more modern introducing benefits such as being mobile friendly, fast, and much more secure which will improve the experience for everyone.
![Camouflage Store homepage as of 2022](camouflage-store-homepage.avif)
_New design as of 2022_
The default border-radius of buttons was also unable to be changed within the theme so I used Shopify's custom CSS feature to override the default styling on all affected elements. This instantly makes the site look more unique compared to others using the same theme. Additionally, keywords have been added into the HTML head for better SEO using Liquid and a custom product metafeild.
```liquid
<meta name="keywords" content="camouflage store, camo, outdoors, devon, uk, ..., {{ product.metafields.custom.keywords }}">
```
Whilst making these modifications to the theme, I found two bugs. Initially with the variant selection options on product pages which I was able to diagnose and fix in communication with the original theme developers. The change was then included within the next update. And lastly, an issue with pages locking up and crashing the entire browser tab or application. According to the developers this was due to the JavaScript in the theme having quotes within option names.
Here's a look at the prior site as it was the day we switched over to using the new redesign.
![Camouflage Store Previous Design](camouflage-store-previous.avif)
_Previous design_
## Branding
Along with the switch of platform, we came to the decision that the domain and overall branding would need to be updated to to go along with the rest of the work being done. I felt it was important however that the original red colour of `#dd3e3e` was kept as it was a key part of the brand from all the way back in 2007.
For the domain, we have gone with [camouflagestore.uk](https://camouflagestore.uk) (and its equavalent .co.uk tld). This was chosen as the location of the store is a key factor in its identity, and having this represented from the geto go meant a lot to the client. The legacy domain of [camouflage-store.com](https://camouflage-store.com) has also been kept since it has also been with Steve since 2007 and holds significant personal value.
## VPS
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)

View file

@ -0,0 +1,53 @@
---
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.**
![Progress 5 for Digital Artefact: Corridor project](troy-lusty-highresscreenshot05012023-2.avif)
_Using Lumen for global illumination._
![Progress 4 for Digital Artefact: Corridor project](troy-lusty-highresscreenshot05012023-1.avif)
_Set the project to use deprecated hardware raytracing instead of Lumen, which resulted in the light spill on the walls being fixed._
![Progress 3 for Digital Artefact: Corridor project](troy-lusty-highresscreenshot00001-squeeze.avif)
_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._
![Progress 2 for Digital Artefact: Corridor project](troy-lusty-hall-1205-2.avif)
_Modelled a new scene design by creating repeatable assets in Blender and utilising collection instancing._
![Progress 1 for Digital Artefact: Corridor project](troy-lusty-troy-lusty-output.avif)
_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_

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,15 @@
---
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://github.com/troylusty/discordbot

Binary file not shown.

View file

@ -0,0 +1,28 @@
---
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_
![Firespline frame](troy-lusty-firespline.avif)
_Extracted frame_
### Assets list
https://www.blendswap.com/blend/26395, https://ambientcg.com/view?id=Rock030, https://ambientcg.com/view?id=Ground033

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -0,0 +1,42 @@
---
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";
## A narrative driven horror prototype done as a university project
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_
![Kikimora hallway screenshot](kikimora-ingame-screenshot-1.avif)
_Screenshot 1_
![Kikimora red hallway screenshot](kikimora-ingame-screenshot-2.avif)
_Screenshot 2_
### External links
[Itch.io page](https://troylusty.itch.io/kikimora)

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1,51 @@
---
title: "Kraken in the Cupboard"
description: "Mythical depiction of the Kraken bursting its way out of a bookcase cupboard."
date: 2021-12-11
updated: 2021-12-11
image: { url: "troy-lusty-kraken.avif", alt: "Kraken in the Cupboard final" }
tags: ["blender"]
categories: ["personal"]
includeHero: true
---
Rendered using Cycles X, with all compositing done inside of Blender.
I originally began making this to see how closely I could recreate my own dining room, but later decided that it could be pushed further and developed into a full scene by adding in some more elements. Created with some CC0 assets from ambientcg, polyhaven, blendswap, unsplash, and avopix.
_Approximate time taken: 28/11/2021 - 11/12/2021_
![Final](troy-lusty-kraken.avif)
_Final_
![Alternate idea](troy-lusty-kraken-alternate.avif)
_Alternate idea_
![Early progress](troy-lusty-kraken-early.avif)
_Early progress_
![Main reference images](troy-lusty-kraken-reference.avif)
_Main reference images_
### Assets list
[Wood Floor 017 texture](https://ambientcg.com/view?id=WoodFloor017), [Wood 049 texture](https://ambientcg.com/view?id=Wood049), [Antique Ceramic Vase 01 model](https://polyhaven.com/a/antique_ceramic_vase_01), [Decorative Book Set 01 model](https://polyhaven.com/a/decorative_book_set_01), https://polyhaven.com/a/mantel_clock_01
https://polyhaven.com/a/WoodenTable_01
https://polyhaven.com/a/tea_set_01
https://polyhaven.com/a/Television_01
https://polyhaven.com/a/ceramic_vase_01
https://ambientcg.com/view?id=PaintedPlaster017
https://polyhaven.com/a/dining_chair_02
https://polyhaven.com/a/standing_picture_frame_02
https://unsplash.com/photos/Bin0C2RtQpI
https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSDQNk0oHUX1hpzBAGuEuCkmWvPDxbH6MNZdQ&usqp=CAU
https://blendswap.com/blend/7571
https://ambientcg.com/view?id=PaintedPlaster015
https://ambientcg.com/view?id=SurfaceImperfections013
https://avopix.com/photo/59076-35mm-film-grain-texture
https://polyhaven.com/a/Lantern_01
https://www.blendswap.com/blend/25903
https://polyhaven.com/a/horse_statue_01

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -0,0 +1,21 @@
---
title: "Logofolio"
description: "An ongoing collection of branding and logos designs."
date: 2021-06-16
updated: 2021-06-16
image: { url: "troy-lusty-logofolio.avif", alt: "Logofolio title" }
collection: true
featured: true
tags: ["blender", "pixelmator", "affinity photo", "affinity designer"]
categories: ["client work"]
---
An ongoing collection of branding and logos designs. Including both 2D and 3D works created inside of various softwares and packages.
Layout inspired by: [citrus.works](https://citrus.works/)
![Logofolio title](troy-lusty-logofolio.avif)
![Header design for @_railz_](troy-lusty-logofolio-railz.avif)
Twitter header and rebrand for [railz](https://twitter.com/@_railz_). Inspired by: [Cloakzy- concept broadcast asset redesign](https://www.behance.net/gallery/100498021/Cloakzy-Concept-Broadcast-Assets)

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View file

@ -0,0 +1,22 @@
---
title: "Megascans Artworks"
description: "A small collection of artworks made with Megascans assets."
date: 2021-01-29
updated: 2021-01-29
collection: true
image: { url: "troy-lusty-forest-fire.avif", alt: "Forest fire" }
tags: ["quixel megascans", "blender"]
categories: ["personal"]
---
![Forest fire](troy-lusty-forest-fire.avif)
_Forest fire_
![Little Nightmares](troy-lusty-little-nightmares.avif)
_Little Nightmares_
![Crypt](troy-lusty-crypt.avif)
_Crypt_

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View file

@ -0,0 +1,54 @@
---
title: "Mortis"
description: "1 Corinthians 15:26 - The last enemy to be destroyed is death."
date: 2022-12-10
updated: 2022-12-10
image:
{
url: "troy-lusty-mortis.avif",
alt: "Final finished artwork for Mortis project",
}
featured: true
tags: ["blender", "davinci resolve"]
categories: ["personal"]
includeHero: true
---
> 1 Corinthians 15:26 - The last enemy to be destroyed is death.
![Final finished artwork for Mortis project](troy-lusty-mortis.avif)
_Final_
![Progress 3 for Mortis project](troy-lusty-mortis-progress-3.avif)
_Progress 3_
![Progress 2 for Mortis project](troy-lusty-mortis-progress-2.avif)
_Progress 2_
![Progress 1 for Mortis project](troy-lusty-mortis-progress-1.avif)
_Progress 1_
### Assets list
- **Three D Scans**
- [Le Transi de Rene de Chalon](https://threedscans.com/musee-des-monuments-francais/le-transi-de-rene-de-chalon)
- [Font Reconstructed](https://threedscans.com/lincoln/reconstructed/)
- [Saint Hugh](https://threedscans.com/lincoln/saint-hughs/)
- [Zenobia in Chains](https://threedscans.com/saint-louis-art-museum/zenobia-in-chains/)
- **Unsplash**
- [blue red and yellow floral glass window](https://unsplash.com/photos/YEXaj7mNiCQ)
- **Textures.com**
- [Arcaded Wood Panels - PBR0716](https://www.textures.com/download/PBR0959/140830)
- [Scratches Overlay - OVL0021](https://www.textures.com/download/Overlays0025/136531)
- [Stains 7 Overlay - OVL0040](https://www.textures.com/download/Overlays0041/137435)
- [Waterplants0017](https://www.textures.com/download/Waterplants0017/14022)
- [TombHeadstone0241](https://www.textures.com/download/TombHeadstone0241/130539)
- **PolyHaven**
- [Concrete Floor 02](https://polyhaven.com/a/concrete_floor_02)
- [Large Grey Tiles](https://polyhaven.com/a/large_grey_tiles)
- **Pexels**
- [Free stock photo of 35mm, grain, texture](https://www.pexels.com/photo/35mm-film-grain-texture-246213/)

Some files were not shown because too many files have changed in this diff Show more