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
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ When the user changes a decision, update the document that owns it in the same c

## Stack

TanStack Start (React 19, Vite, Nitro), TypeScript, React Flow, Astryx with Tailwind, Motion, jSquash
TanStack Start (React 19, Vite, Nitro), TypeScript, React Flow, Astryx with Tailwind, Motion, GSAP, jSquash
codecs in Web Workers, OPFS, PostHog and Sentry. Drizzle, PostgreSQL and Better Auth are dormant
until cloud work: keep them compiling and build version 1 features without them.

Expand Down Expand Up @@ -64,6 +64,11 @@ Tailwind utilities such as `bg-surface`, `text-primary` and `rounded-lg`.
- Set colours, type and other tokens in `src/features/theme/hexlode-theme.ts`, then run
`pnpm theme:build`. Every colour needs a light and a dark value.
- Style the Studio canvas with the same tokens and hide the React Flow attribution.
- Nest corners: an inner corner is the outer corner minus the padding between them, such as a
`rounded-lg` (12px) card with 8px padding around `rounded` (4px) images. Astryx maps `rounded-xl`
to the 28px page radius, so keep it off cards.
- Animate interface elements with Motion. Direct home page scenes with GSAP through `useScene`
and the timings in `src/features/home/constants.ts` (ADR 0009).

## Commits

Expand Down
39 changes: 32 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
# Hexlode

[![CI](https://github.com/pixelactstudio/hexlode/actions/workflows/ci.yml/badge.svg)](https://github.com/pixelactstudio/hexlode/actions/workflows/ci.yml)
[![CodeQL](https://github.com/pixelactstudio/hexlode/actions/workflows/codeql.yml/badge.svg)](https://github.com/pixelactstudio/hexlode/actions/workflows/codeql.yml)

Open-source image processing in the browser: quick tools for converting, compressing, resizing, cropping,
rotating and stripping metadata, and a node-based Studio for batch pipelines. Images never leave your device.
<div align="center">
<img src="./public/hexlode-mark.svg" alt="Hexlode" width="64" height="64" />
<h1>Hexlode</h1>
<p>Open-source image processing that runs in your browser.</p>
</div>

<p align="center">
<a href="https://github.com/pixelactstudio/hexlode/actions/workflows/ci.yml">
<img src="https://github.com/pixelactstudio/hexlode/actions/workflows/ci.yml/badge.svg" alt="CI" />
</a>
<a href="https://github.com/pixelactstudio/hexlode/actions/workflows/codeql.yml">
<img src="https://github.com/pixelactstudio/hexlode/actions/workflows/codeql.yml/badge.svg" alt="CodeQL" />
</a>
<a href="./LICENSE">
<img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="Apache-2.0 license" />
</a>
</p>

Hexlode has quick tools for converting, compressing, resizing, cropping, rotating and stripping
metadata, and a node-based Studio for running many images through the same pipeline. Images are
processed on your device, without uploading them.

Use it at [hexlode.damnlabs.com](https://hexlode.damnlabs.com). Hexlode is made by [Damn Labs](https://damnlabs.com),
a [Pixelact Studio](https://pixelactstudio.com) product.

## Principles

- Process images on the user's device.
- Make the Studio canvas show real work: progress, results and errors.
- Never send image bytes, filenames, thumbnails or metadata to analytics.

## Run locally

Expand Down Expand Up @@ -57,3 +80,5 @@ engine, [CONTEXT.md](./CONTEXT.md) the vocabulary and [docs/adr/](./docs/adr/) t

[Apache License 2.0](./LICENSE). Copyright 2026 Dev Talan. The jSquash codecs keep their own
licences, listed in `node_modules/@jsquash/*/LICENSE` and bundled with the app.

An open-source project by [Pixelact Studio](https://pixelactstudio.com).
33 changes: 33 additions & 0 deletions docs/adr/0009-gsap-scenes-motion-interface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# GSAP for home page scenes, Motion for the interface

The home page's Studio pictures are directed scenes: one GSAP timeline each, with beats that
follow one another, a pointer that acts them out, and a rest on the last frame before they repeat.
Motion stays for interface motion such as presses, menus, swapping labels, the top bar and the
footer glow. Before this, every picture was a set of Motion loops with their own periods, so
several things moved at different speeds at once and nothing showed cause and effect.

A scene is built with `useScene` in `src/features/home/scene.ts`. It creates a paused timeline,
lets the scene add its tweens, starts it with ScrollTrigger when the scene scrolls into view after
a delay for its column, and pauses it off screen. With reduced motion it jumps to the scene's
`poster` label and stays there. Timings and easings come from `src/features/home/constants.ts`.
GSAP moves elements; React state that a scene changes, such as a label or a count, is set from
timeline callbacks, and Motion animates the swap.

## Considered options

- **Motion only.** It handles interface motion well, but sequencing beats across elements means
chains of timers and state, and it has no timeline to pause, seek or jump to a still frame.
- **Rive.** Its animations are drawn in the Rive editor and need its runtime. Our pictures are
built from the Studio's own node icons and colour tokens, so they stay sharp and follow the
colour mode without extra artwork. Worth another look for illustration or a mascot.

## Consequences

- GSAP ships under its own no-charge licence, not an open-source one. It allows use in any
project, Hexlode's Apache 2.0 code included, and all of its plugins are free.
- GSAP and ScrollTrigger load with the home page only. Tool pages and the Studio do not pay for
them.
- A tween that sets its start values when the timeline is built (`fromTo`, `from`) shows them at
once. Use `immediateRender: false` when the start should only appear when the tween plays.
- GSAP rounds pixel values, so a fraction of an SVG path length, as used for drawing edges and
beams, is tweened as an attribute: `attr: { 'stroke-dashoffset': 0.2 }`.
14 changes: 11 additions & 3 deletions idea.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Hexlode product

> Updated: 2026-09-27 (Phase 1 interface redesign: animated home page, top bar, tool pages)
> Updated: 2026-09-28 (directed Studio scenes on the home page, footer credits and wordmark glow)
> Delivery plan: [implementation.md](./implementation.md). Vocabulary: [CONTEXT.md](./CONTEXT.md).
> Decisions and their reasons: [docs/adr/](./docs/adr/).

Expand Down Expand Up @@ -45,13 +45,21 @@ it says the work can run on the device without uploading, never that nothing is
that there are no accounts. Animations run only while on screen, start from a still first frame
rendered on the server, and stop when the system asks for reduced motion.

The Studio section's pictures are short directed scenes that follow one batch of 240 photos: the
graph builds and the batch runs through it, a pointer changes a crop and the preview reframes, four
workers share the last images of the batch, an edited setting reruns only the changed steps, and
the pipeline is saved as a tool. One thing moves at a time, each scene rests on its last frame
before it plays again, and scenes side by side start one after another. With reduced motion each
scene shows one still frame.

Every page shares one frame that stays mounted while pages change. The top bar holds the name, a
Tools menu that opens on click and lists the quick tools with a short line each, the Studio, a
GitHub link and the colour mode. The bar is opaque. On pages that scroll it lines up with the
1200-pixel column and folds into a floating dock once the page scrolls; on the Studio it spans the
window, and moving between the two animates its width. On phones its links move into a menu
button. The footer holds a line about Hexlode, the links to the tools, the Studio, the privacy
page, the codec licences and the repository, and a large dotted wordmark.
button. The footer holds a line about Hexlode, a credit to Damn Labs and Pixelact Studio, the links to
the tools, the Studio, the privacy page, the codec licences, the repository and the other Damn Labs
sites, and a large dotted wordmark whose dots brighten in a circle under the pointer.

The colour mode is dark, light or the system's. Dark is the default and is pitch dark. The choice
is kept in browser storage and applied before the page paints, so a light page never flashes dark.
Expand Down
5 changes: 3 additions & 2 deletions implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ Active phase: **Phase 1**.
### Application

13. Home page and the six quick tools: Convert, Compress, Resize, Crop, Rotate, Strip metadata.
The home page animates with Motion (`motion/react`) and shows a Studio screenshot taken from a
real run. The site frame lives in the root route so the top bar animates between pages. The
The home page shows a Studio screenshot taken from a real run. Its Studio scenes are GSAP
timelines started by ScrollTrigger (`useScene` in `src/features/home/scene.ts`); interface
motion elsewhere uses Motion (`motion/react`). See ADR 0009. The site frame lives in the root route so the top bar animates between pages. The
Hexlode theme in `src/features/theme/`, with dark, light and system colour modes and a
self-hosted Figtree font.
14. Studio: node library with every category, drag, search, category filter and a folded rail,
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"dependencies": {
"@astryxdesign/core": "^0.2.0",
"@fontsource-variable/figtree": "^5.3.0",
"@gsap/react": "^2.1.2",
"@jsquash/avif": "^2.1.1",
"@jsquash/jpeg": "^1.6.0",
"@jsquash/jxl": "^1.3.0",
Expand Down Expand Up @@ -66,6 +67,7 @@
"dotenv-cli": "^11.0.0",
"drizzle-kit": "^0.31.9",
"drizzle-orm": "^0.45.1",
"gsap": "^3.15.0",
"lucide-react": "^1.28.0",
"motion": "^13.4.4",
"nitro": "3.0.260610-beta",
Expand Down
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions src/features/app-shell/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
export const REPOSITORY_URL = 'https://github.com/pixelactstudio/hexlode'

/** Damn Labs, Pixelact Studio's lab for experimental software, which makes Hexlode. */
export const DAMN_LABS_URL = 'https://damnlabs.com'

export const PIXELACT_STUDIO_URL = 'https://pixelactstudio.com'

/** EnvSift, Damn Labs' first product. */
export const ENVSIFT_URL = 'https://envsift.damnlabs.com'

/** The widest a page's content gets. The top bar lines up with it on every page but the Studio. */
export const PAGE_WIDTH = 1200

Expand Down
63 changes: 63 additions & 0 deletions src/features/app-shell/footer-wordmark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
motion,
useMotionTemplate,
useMotionValue,
useReducedMotion,
useSpring,
} from 'motion/react'
import type { PointerEvent } from 'react'

/** Radius, in pixels, of the circle of dots that lights up under the pointer. */
const GLOW_RADIUS = 180

const WORDMARK =
'block select-none bg-[length:5px_5px] bg-clip-text text-center font-bold text-[clamp(88px,19vw,260px)] text-transparent leading-[0.8] tracking-[-0.04em]'

/**
* The large dotted "Hexlode" at the foot of every page. Under the pointer its dots brighten in a
* circle that fades out towards the edge and trails the pointer a little, then dims when the
* pointer leaves. Only the dots light up; the gaps between them stay dark.
*/
export function FooterWordmark() {
const reduced = useReducedMotion()
const spring = reduced ? { duration: 0 } : { stiffness: 260, damping: 30, mass: 0.6 }
const x = useSpring(useMotionValue(0), spring)
const y = useSpring(useMotionValue(0), spring)
const strength = useSpring(0, reduced ? { duration: 0 } : { stiffness: 120, damping: 24 })
const mask = useMotionTemplate`radial-gradient(circle ${GLOW_RADIUS}px at ${x}px ${y}px, black, rgb(0 0 0 / 0.35) 45%, transparent 100%), linear-gradient(to bottom, black 40%, transparent)`

function move(event: PointerEvent<HTMLSpanElement>) {
const box = event.currentTarget.getBoundingClientRect()
const left = event.clientX - box.left
const top = event.clientY - box.top
// Enter at the pointer rather than sliding in from the last spot the glow was.
if (strength.get() < 0.01) {
x.jump(left)
y.jump(top)
}
x.set(left)
y.set(top)
strength.set(1)
}

return (
<span
aria-hidden="true"
className="relative block"
onPointerMove={move}
onPointerLeave={() => strength.set(0)}
>
<span
className={`${WORDMARK} bg-[radial-gradient(var(--color-border-strong)_1px,transparent_1.4px)] [mask-image:linear-gradient(to_bottom,black_40%,transparent)]`}
>
Hexlode
</span>
<motion.span
className={`${WORDMARK} pointer-events-none absolute inset-0 bg-[radial-gradient(var(--color-text-primary)_1.2px,transparent_1.6px)] [mask-composite:intersect]`}
style={{ maskImage: mask, opacity: strength }}
>
Hexlode
</motion.span>
</span>
)
}
2 changes: 1 addition & 1 deletion src/features/app-shell/icon-tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const TONES: Record<Tone, string> = {
gray: 'bg-gray-subtle text-gray-vivid',
}

const SIZES = { sm: 'size-7 rounded-md', md: 'size-9 rounded-lg', lg: 'size-12 rounded-xl' }
const SIZES = { sm: 'size-7 rounded-sm', md: 'size-9 rounded-md', lg: 'size-12 rounded-lg' }

/** An icon on a tinted square, used to tell tools and node categories apart at a glance. */
export function IconTile({
Expand Down
Loading
Loading