first commit
15
.dockerignore
Normal 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
|
@ -0,0 +1 @@
|
|||
* text=auto
|
1
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* @troylusty
|
6
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
53
.github/workflows/docker.yml
vendored
Normal 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
|
@ -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
|
@ -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
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.astro",
|
||||
"options": {
|
||||
"parser": "astro"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
12
Dockerfile
Normal 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
|
@ -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
|
@ -0,0 +1,60 @@
|
|||
# Astro Portfolio: Personal Website
|
||||
|
||||

|
||||
|
||||
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
|
@ -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
37
package.json
Normal 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"
|
||||
}
|
||||
}
|
4
public/.well-known/security.txt
Normal 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
After Width: | Height: | Size: 2.1 KiB |
BIN
public/assets/gradient.avif
Normal file
After Width: | Height: | Size: 570 B |
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
9
public/favicon.svg
Normal 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
|
@ -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-----
|
35
src/components/Accordion.astro
Normal 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>
|
106
src/components/Article.astro
Normal 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>
|
30
src/components/Education.astro
Normal 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>
|
64
src/components/Footer.astro
Normal 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"
|
||||
>© {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>
|
17
src/components/FormattedDate.astro
Normal 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
|
@ -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>
|
19
src/components/Header.astro
Normal 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
|
@ -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>
|
5
src/components/Prose.astro
Normal 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>
|
49
src/components/Showcase.astro
Normal 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} /> • Collection
|
||||
</span>
|
||||
) : (
|
||||
<FormattedDate date={collection.data.date} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</article>
|
23
src/components/ShowcasePage.astro
Normal 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>
|
27
src/components/Skills.astro
Normal 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>
|
5
src/components/SkinnyCenter.astro
Normal 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
|
@ -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
|
@ -0,0 +1,102 @@
|
|||
import type { Metadata, Site } from "@types";
|
||||
|
||||
export const SITE: Site = {
|
||||
TITLE: "Troy Lusty",
|
||||
DESCRIPTION:
|
||||
"Hi, my name is Troy and I’m 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
|
@ -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 };
|
11
src/content/education/kennicott.md
Normal 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"
|
||||
---
|
11
src/content/education/kevicc.md
Normal 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"
|
||||
---
|
11
src/content/education/sdc.md
Normal 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"
|
||||
---
|
12
src/content/education/ucsd.md
Normal 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
|
||||
---
|
7
src/content/education/uop.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
institution: "University of Plymouth"
|
||||
qualification: "BA (Hons) Game Arts and Design"
|
||||
grades: ["1st year: Uncommenced"]
|
||||
dateStart: "2024"
|
||||
dateEnd: "2025"
|
||||
---
|
BIN
src/content/posts/libreboot-on-an-x230/flashing.avif
Normal file
After Width: | Height: | Size: 57 KiB |
144
src/content/posts/libreboot-on-an-x230/index.mdx
Normal 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.
|
||||
|
||||

|
||||
|
||||
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).
|
23
src/content/projects/3d-package-design/index.mdx
Normal 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_
|
||||
|
||||

|
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 8.9 KiB |
BIN
src/content/projects/a-long-way-down/alwd-early-forest.avif
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/content/projects/a-long-way-down/alwd-img1.avif
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/content/projects/a-long-way-down/alwd-img2.avif
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/content/projects/a-long-way-down/alwd-img3.avif
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
src/content/projects/a-long-way-down/alwd-img4.avif
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/content/projects/a-long-way-down/alwd-img5.avif
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/content/projects/a-long-way-down/alwd-img6.avif
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/content/projects/a-long-way-down/alwd-style-experiment.avif
Normal file
After Width: | Height: | Size: 7.1 KiB |
66
src/content/projects/a-long-way-down/index.mdx
Normal 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_
|
||||
|
||||

|
||||
|
||||
_Intro_
|
||||
|
||||

|
||||
|
||||
_Forest_
|
||||
|
||||

|
||||
|
||||
_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)
|
23
src/content/projects/astronaut/index.mdx
Normal 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 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)
|
BIN
src/content/projects/astronaut/troy-lusty-astronaut.avif
Normal file
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 103 KiB |
After Width: | Height: | Size: 99 KiB |
After Width: | Height: | Size: 33 KiB |
61
src/content/projects/camouflage-store/index.mdx
Normal 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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
_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.
|
||||
|
||||

|
||||
_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)
|
BIN
src/content/projects/digital-artifact-corridor/deltakey.webm
Normal file
53
src/content/projects/digital-artifact-corridor/index.mdx
Normal 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.**
|
||||
|
||||

|
||||
|
||||
_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_
|
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 26 KiB |
BIN
src/content/projects/discord-bot/discord.avif
Normal file
After Width: | Height: | Size: 2.4 KiB |
15
src/content/projects/discord-bot/index.mdx
Normal 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
|
BIN
src/content/projects/firespline/firespline.webm
Normal file
28
src/content/projects/firespline/index.mdx
Normal 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_
|
||||
|
||||

|
||||
|
||||
_Extracted frame_
|
||||
|
||||
### Assets list
|
||||
|
||||
https://www.blendswap.com/blend/26395, https://ambientcg.com/view?id=Rock030, https://ambientcg.com/view?id=Ground033
|
BIN
src/content/projects/firespline/troy-lusty-firespline.avif
Normal file
After Width: | Height: | Size: 9.3 KiB |
42
src/content/projects/kikimora/index.mdx
Normal 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_
|
||||
|
||||

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

|
||||
|
||||
_Screenshot 2_
|
||||
|
||||
### External links
|
||||
|
||||
[Itch.io page](https://troylusty.itch.io/kikimora)
|
BIN
src/content/projects/kikimora/kikimora-gameplay.webm
Normal file
BIN
src/content/projects/kikimora/kikimora-ingame-screenshot-1.avif
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/content/projects/kikimora/kikimora-ingame-screenshot-2.avif
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/content/projects/kikimora/kikimora-titlecard.avif
Normal file
After Width: | Height: | Size: 4.3 KiB |
51
src/content/projects/kraken-in-the-cupboard/index.mdx
Normal 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_
|
||||
|
||||

|
||||
|
||||
_Alternate idea_
|
||||
|
||||

|
||||
|
||||
_Early progress_
|
||||
|
||||

|
||||
|
||||
_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
|
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 37 KiB |
21
src/content/projects/logofolio/index.mdx
Normal 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/)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
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)
|
BIN
src/content/projects/logofolio/troy-lusty-logofolio-railz.avif
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
src/content/projects/logofolio/troy-lusty-logofolio.avif
Normal file
After Width: | Height: | Size: 6.9 KiB |
22
src/content/projects/megascans-artworks/index.mdx
Normal 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_
|
||||
|
||||

|
||||
|
||||
_Little Nightmares_
|
||||
|
||||

|
||||
|
||||
_Crypt_
|
BIN
src/content/projects/megascans-artworks/troy-lusty-crypt.avif
Normal file
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 117 KiB |
After Width: | Height: | Size: 72 KiB |
54
src/content/projects/mortis/index.mdx
Normal 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_
|
||||
|
||||

|
||||
|
||||
_Progress 3_
|
||||
|
||||

|
||||
|
||||
_Progress 2_
|
||||
|
||||

|
||||
|
||||
_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/)
|