Skip to content

Commit 5dba3ba

Browse files
authored
feat(site): animate the landing page mascot (#53)
## Related Issue No linked issue. This change follows a direct maintainer request for the landing page. ## Problem The landing page used a static mascot. The supplied animation was 6.98 MB, included audio, contained dark fade frames, and showed an opaque rectangular background against the page. ## What changed - Replaced the static hero mascot with an 811 KB H.264 animation with fast-start metadata and no audio. - Trimmed the faded frames, built a forward-and-reverse loop, masked the video edges into the page background, and removed the eyebrow artwork from every frame. - Kept the mascot itself as an accessible play/pause target, without a visible control overlay, and added live reduced-motion preference handling. ## Verification - `pnpm --filter @pythoughts/site build` - Browser checks for autoplay, mascot play/pause behavior, live reduced-motion changes, responsive layout, and edge blending - FFmpeg contact-sheet inspection across the installed loop - Pixel-mask verification across all 239 frames: zero residual eyebrow pixels in both tracked regions - `pnpm lint` (passes with existing warnings) - Pre-push gate passed: attribution scan, sherif, lint, Nix hash freshness, changed-package typecheck, and changed tests - Full `pnpm test`: 650 passed, 11 skipped, and 1 unrelated environment-sensitive failure in `test/cli/export.test.ts` because the non-interactive harness reported no shell ## Checklist - [x] I have read the [CONTRIBUTING](https://github.com/Pythoughts-labs/pythinker-code/blob/main/CONTRIBUTING.md) document. - [x] I have linked a related issue, or explained the problem above. - [ ] I have added tests that prove my feature works. (No automated site UI test harness; browser and media checks cover this change.) - [x] Ran `gen-changesets` skill, or this PR needs no changeset. (Private marketing site only; no released package changes.) - [x] Ran `gen-docs` skill, or this PR needs no doc update. (The landing page is the changed user-facing surface.)
1 parent 97afb85 commit 5dba3ba

2 files changed

Lines changed: 79 additions & 2 deletions

File tree

792 KB
Binary file not shown.

apps/site/src/App.vue

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ const vscodeInstallCommand = 'code --install-extension pythoughts.pythinker-code
6161
6262
const copiedCommand = ref('');
6363
const mobileMenu = ref(null);
64+
const heroVideo = ref(null);
65+
const heroVideoPlaying = ref(false);
6466
const menuButton = ref(null);
6567
const menuOpen = ref(false);
6668
const activeTerminalDemo = ref(terminalDemos[0]);
@@ -70,6 +72,7 @@ let resetTimer;
7072
let menuCloseTimer;
7173
let terminalTimer;
7274
let revealObserver;
75+
let reducedMotionQuery;
7376
let menuOpenedByHover = false;
7477
7578
function fallbackCopy(text) {
@@ -168,11 +171,32 @@ function onDocumentKeydown(event) {
168171
if (event.key === 'Escape') closeMenu();
169172
}
170173
174+
async function playHeroVideo() {
175+
try {
176+
await heroVideo.value?.play();
177+
} catch {
178+
heroVideoPlaying.value = false;
179+
}
180+
}
181+
182+
function syncHeroMotionPreference() {
183+
if (reducedMotionQuery.matches) heroVideo.value?.pause();
184+
else void playHeroVideo();
185+
}
186+
187+
function toggleHeroVideo() {
188+
if (heroVideo.value?.paused) void playHeroVideo();
189+
else heroVideo.value?.pause();
190+
}
191+
171192
onMounted(() => {
172193
document.addEventListener('pointerdown', onDocumentPointerdown);
173194
document.addEventListener('keydown', onDocumentKeydown);
195+
reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
196+
reducedMotionQuery.addEventListener('change', syncHeroMotionPreference);
197+
syncHeroMotionPreference();
174198
playTerminalDemo();
175-
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
199+
if (reducedMotionQuery.matches) return;
176200
177201
revealObserver = new IntersectionObserver((entries) => {
178202
for (const entry of entries) {
@@ -190,6 +214,7 @@ onUnmounted(() => {
190214
clearTimeout(menuCloseTimer);
191215
clearTerminalPlayback();
192216
revealObserver?.disconnect();
217+
reducedMotionQuery?.removeEventListener('change', syncHeroMotionPreference);
193218
document.removeEventListener('pointerdown', onDocumentPointerdown);
194219
document.removeEventListener('keydown', onDocumentKeydown);
195220
});
@@ -225,7 +250,28 @@ onUnmounted(() => {
225250
<main id="top">
226251
<header class="hero container">
227252
<div class="hero-copy">
228-
<PythinkerMascot :width="72" :height="90" />
253+
<button
254+
class="hero-mascot-video"
255+
type="button"
256+
:aria-label="heroVideoPlaying ? 'Pause mascot animation' : 'Play mascot animation'"
257+
@click="toggleHeroVideo"
258+
>
259+
<video
260+
ref="heroVideo"
261+
autoplay
262+
muted
263+
loop
264+
playsinline
265+
preload="metadata"
266+
width="1280"
267+
height="720"
268+
aria-hidden="true"
269+
@play="heroVideoPlaying = true"
270+
@pause="heroVideoPlaying = false"
271+
>
272+
<source src="/pythinker-hero-clean-v2.mp4" type="video/mp4" />
273+
</video>
274+
</button>
229275
<h1>Pythinker Code</h1>
230276
<p class="hero-accent">Think first, then code.</p>
231277
<p class="hero-lead">An open-source AI engineering agent for your terminal. It reads your repo, edits files, runs commands, and iterates until the job is done.</p>
@@ -722,6 +768,37 @@ onUnmounted(() => {
722768
line-height: 1.38;
723769
}
724770
771+
.hero-mascot-video {
772+
position: relative;
773+
display: block;
774+
width: clamp(160px, 16vw, 200px);
775+
margin-inline: auto;
776+
padding: 0;
777+
border: 0;
778+
appearance: none;
779+
aspect-ratio: 4 / 3;
780+
background: transparent;
781+
color: var(--ink);
782+
cursor: pointer;
783+
}
784+
785+
.hero-mascot-video:focus-visible {
786+
outline: 2px solid var(--accent);
787+
outline-offset: 4px;
788+
border-radius: var(--radius-sm);
789+
}
790+
791+
.hero-mascot-video video {
792+
display: block;
793+
width: 100%;
794+
height: 100%;
795+
object-fit: cover;
796+
-webkit-mask-image: radial-gradient(ellipse 50% 50% at center, black 58%, transparent 100%);
797+
mask-image: radial-gradient(ellipse 50% 50% at center, black 58%, transparent 100%);
798+
mix-blend-mode: multiply;
799+
pointer-events: none;
800+
}
801+
725802
.works-with {
726803
display: flex;
727804
width: 100%;

0 commit comments

Comments
 (0)