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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions assets/css/_home.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,90 @@
animation: home-rise .9s var(--ease) .1s both;
}

// ---------- intro film (shortcodes/film.html, tools/film/) ----------
// The box has its size before anything loads (aspect-ratio), so no shift.
.film {
position: relative;
margin: clamp(48px, 8vh, 96px) 0 0;
aspect-ratio: 16 / 9;
border-radius: 12px;
overflow: hidden;
background: #09090a;
box-shadow: 0 0 0 1px var(--line);

img,
video {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
margin: 0;
object-fit: cover;
}

video {
opacity: 0;
transition: opacity .3s var(--ease);
}

&.on video {
opacity: 1;
}
}

.film-btn {
position: absolute;
right: 14px;
bottom: 14px;
z-index: 2;
display: grid;
place-items: center;
width: 44px;
height: 44px;
padding: 0;
border: 1px solid rgba(255, 255, 255, .16);
border-radius: 50%;
background: rgba(9, 9, 10, .62);
color: #ecebe7;
cursor: pointer;
opacity: .72;
transition: opacity .2s var(--ease), transform .2s var(--ease);

&:hover,
&:focus-visible {
opacity: 1;
}

&:active {
transform: scale(.94);
}

svg {
display: none;
width: 16px;
height: 16px;
fill: currentColor;
}

&[data-state="play"] .i-play,
&[data-state="pause"] .i-pause,
&[data-state="replay"] .i-replay {
display: block;
}
}

@media (max-width: 640px) {
.film {
aspect-ratio: 4 / 5;
}

.film-btn {
right: 10px;
bottom: 10px;
}
}

// ---------- small screens ----------
@media (max-width: 960px) {
.case {
Expand Down
26 changes: 26 additions & 0 deletions assets/js/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,32 @@
}, { threshold: .3 }).observe(vid);
});

/* intro film: plays once while on screen, pauses off screen. Reduced motion
and Save-Data / slow connections keep the poster until someone presses play. */
var film = document.getElementById('film');
if (film) {
var fv = film.querySelector('video'), fb = film.querySelector('.film-btn');
var conn = navigator.connection || {};
var auto = !reduce && !conn.saveData && !/(^|-)2g|3g/.test(conn.effectiveType || '');
var wanted = false;
var setBtn = function (s, label) { fb.dataset.state = s; fb.setAttribute('aria-label', label); };
var play = function () {
fv.preload = 'auto';
fv.play().then(function () { film.classList.add('on'); setBtn('pause', 'Pause the intro film'); }, function () {});
};
fv.addEventListener('ended', function () { wanted = false; setBtn('replay', 'Play the intro film again'); });
fv.addEventListener('pause', function () { if (!fv.ended) setBtn('play', 'Play the intro film'); });
fb.addEventListener('click', function () {
if (fv.paused) { wanted = true; if (fv.ended) fv.currentTime = 0; play(); }
else { wanted = false; auto = false; fv.pause(); }
});
new IntersectionObserver(function (es) {
var vis = es[0].isIntersecting;
if (vis && (auto || wanted) && fv.paused && !fv.ended) { wanted = true; play(); }
else if (!vis && !fv.paused) fv.pause();
}, { threshold: .5 }).observe(film);
}

/* contact: click copies the address */
var mail = document.getElementById('mail'), hint = document.getElementById('copyhint');
if (mail && hint) {
Expand Down
5 changes: 4 additions & 1 deletion content/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ description = "Isaac Lins. Application developer and security analyst at Swissco
<!--
The home page. The big name above this is the site title (config.toml).
Projects and "Also built" are in data/projects.toml.
Shortcodes: layouts/shortcodes/{meta,proof,sec,about,work,writing,contact}.html
Shortcodes: layouts/shortcodes/{meta,proof,film,sec,about,work,writing,contact}.html
The intro film is made in code: tools/film/ (README there).
-->

{{< meta >}}
Expand All @@ -28,6 +29,8 @@ engineer and build systems that make it to production.
AI | tools & agents, from prompt to production
{{< /proof >}}

{{< film >}}

{{< sec "about" >}}

{{< about pull="I started as a developer and moved into security. Both stuck." >}}
Expand Down
Binary file added images/home/intro-16x9.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/home/intro-4x5.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions layouts/shortcodes/film.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{{- /* Home: the intro film (source and render script in tools/film/).
16:9 on wide screens, the 4:5 cut on phones. The picture underneath is
the poster; the video only loads when home.js decides to play it:
on screen, no reduced motion, no Save-Data. One button: play / pause / replay. */ -}}
<figure class="film rv" id="film">
<picture>
<source media="(max-width: 640px)" srcset="{{ "images/home/intro-4x5.webp" | relURL }}" width="720" height="900">
<img src="{{ "images/home/intro-16x9.webp" | relURL }}" alt="" width="1920" height="1080" loading="lazy" decoding="async">
</picture>
<video muted playsinline preload="none" aria-label="Intro film: I build applications and ship AI tools, from a prompt to production.">
<source media="(max-width: 640px)" src="{{ "video/intro-4x5.webm" | relURL }}" type="video/webm">
<source media="(max-width: 640px)" src="{{ "video/intro-4x5.mp4" | relURL }}" type="video/mp4">
<source src="{{ "video/intro-16x9.webm" | relURL }}" type="video/webm">
<source src="{{ "video/intro-16x9.mp4" | relURL }}" type="video/mp4">
</video>
<button class="film-btn" type="button" aria-label="Play the intro film" data-state="play">
<svg class="i-play" viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.5v9l7-4.5z"/></svg>
<svg class="i-pause" viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 3.5h2.5v9H4.5zM9 3.5h2.5v9H9z"/></svg>
<svg class="i-replay" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 3a5 5 0 1 1-4.6 3H1.8L4.3 3l2.5 3H5.1A3.4 3.4 0 1 0 8 4.6z"/></svg>
</button>
</figure>
Binary file added static/video/intro-16x9.mp4
Binary file not shown.
Binary file added static/video/intro-16x9.webm
Binary file not shown.
Binary file added static/video/intro-4x5.mp4
Binary file not shown.
Binary file added static/video/intro-4x5.webm
Binary file not shown.
2 changes: 2 additions & 0 deletions tools/film/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out/
node_modules
49 changes: 49 additions & 0 deletions tools/film/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Intro film

The 18-second intro film on the home page, made entirely in code: one canvas,
a fixed timeline, rendered frame by frame. No stock footage, no AI video.

| File | What |
|---|---|
| `film.html` | Loads the two scripts. Open `film.html?w=1920&h=1080&f=500` (via any static server from the repo root) to see frame 500. |
| `film.js` | The film. `T` at the top is the timeline (seconds); one `shotN(t)` function per shot. |
| `icons.js` | The cast: one 16×16 pixel icon per project on the home page, drawn as grids or shape functions. |
| `render.mjs` | Playwright drives the page frame by frame (`FILM.render(n)`), pipes PNGs into ffmpeg. |
| `build.sh` | Renders both cuts and encodes everything into `out/` (git-ignored). |

## Script

| Time | Shot |
|---|---|
| 0.0 | black: "I build applications." |
| 2.2 | paper: "and ship AI tools.", the seven projects print in around it |
| 4.8 | the ring folds into a `git log --graph` with the real commit hashes; `HEAD -> main` lands on Stewie |
| 9.3 | black: "from a prompt", a prompt types a real request from Stewie's post |
| 11.6 | paper: "to production.", the prompt becomes `minecraft.isaaclins.com · live` |
| 14.3 | black: the live dot pulls the cast in and types `////////// isaaclins.com` |

Palette, font and the `//////////` mark come from the site (`assets/css/main.scss`,
`static/fonts/`). Time is driven, never read from a clock, so frame n is
always the same picture.

## Render

```sh
cd tools/film
npm i playwright && npx playwright install chromium-headless-shell # once
./build.sh # masters + every web file
./build.sh encode # only re-encode from the masters
```

About 1,080 frames per cut. Run it on a strong machine. Outputs in `out/`:

| File | Use |
|---|---|
| `intro-16x9.mp4` / `.webm` | home page, wide screens → `static/video/` |
| `intro-4x5-720.mp4` / `.webm` | home page, phones → `static/video/intro-4x5.*` |
| `intro-16x9-poster.webp`, `intro-4x5-poster.webp` | posters → `images/home/intro-16x9.webp`, `intro-4x5.webp` |
| `intro-4x5-linkedin.mp4` | 1080×1350, silent, for LinkedIn |
| `intro-16x9-hq.mp4` | 1080p high quality, for sharing |

The film block on the home page is `layouts/shortcodes/film.html` (+ `.film` in
`assets/css/_home.scss`, the play/pause logic in `assets/js/home.js`).
29 changes: 29 additions & 0 deletions tools/film/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Render both cuts and encode every deliverable into tools/film/out/.
# ./build.sh masters + web files + poster
# ./build.sh encode only re-encode from existing masters
# ./build.sh masters only render the masters
# Needs node + playwright (chromium) + ffmpeg on PATH. Heavy: run it on a
# strong machine, not a laptop on battery (1080 frames per cut).
set -euo pipefail
cd "$(dirname "$0")"
mkdir -p out
if [ "${1:-}" != "encode" ]; then
node render.mjs --w 1920 --h 1080 --out out/master-16x9.mp4
node render.mjs --w 1080 --h 1350 --out out/master-4x5.mp4
fi
[ "${1:-}" = "masters" ] && exit 0
X264='-c:v libx264 -preset veryslow -tune animation -profile:v high -pix_fmt yuv420p -movflags +faststart -an'
VP9='-c:v libvpx-vp9 -b:v 0 -row-mt 1 -deadline good -cpu-used 1 -pix_fmt yuv420p -an'
# 16:9, the homepage and anywhere else
ffmpeg -v error -y -i out/master-16x9.mp4 $X264 -crf 27 out/intro-16x9.mp4
ffmpeg -v error -y -i out/master-16x9.mp4 $VP9 -crf 40 out/intro-16x9.webm
# 4:5, phones on the homepage (720 wide) and LinkedIn (full 1080x1350, silent for now)
ffmpeg -v error -y -i out/master-4x5.mp4 -vf scale=720:900:flags=lanczos $X264 -crf 27 out/intro-4x5-720.mp4
ffmpeg -v error -y -i out/master-4x5.mp4 -vf scale=720:900:flags=lanczos $VP9 -crf 40 out/intro-4x5-720.webm
ffmpeg -v error -y -i out/master-4x5.mp4 $X264 -crf 18 out/intro-4x5-linkedin.mp4
ffmpeg -v error -y -i out/master-16x9.mp4 $X264 -crf 18 out/intro-16x9-hq.mp4
# posters: the finished git log (6.8 s)
ffmpeg -v error -y -ss 6.8 -i out/master-16x9.mp4 -frames:v 1 -c:v libwebp -quality 82 out/intro-16x9-poster.webp
ffmpeg -v error -y -ss 6.8 -i out/master-4x5.mp4 -frames:v 1 -vf scale=720:900:flags=lanczos -c:v libwebp -quality 82 out/intro-4x5-poster.webp
ls -la out
9 changes: 9 additions & 0 deletions tools/film/film.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<!-- The intro film. Open with a static server from the repo root (render.mjs
does that), e.g. /tools/film/film.html?w=1920&h=1080&f=500 shows frame 500. -->
<meta charset="utf-8">
<title>isaaclins.com intro film</title>
<style>html,body{margin:0;background:#000}canvas{display:block}</style>
<canvas id="c"></canvas>
<script src="icons.js"></script>
<script src="film.js"></script>
Loading
Loading