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
33 changes: 24 additions & 9 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates


# Dependabot version updates.
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions" # See documentation for possible values
directory: "/" # Location of package manifests
# JavaScript dependencies (package.json + bun.lock)
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
groups:
# One PR per week for routine minor/patch bumps instead of one per package
minor-and-patch:
update-types:
- "minor"
- "patch"

# GitHub Actions used in .github/workflows
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
groups:
actions:
update-types:
- "minor"
- "patch"
Binary file added app/apple-icon.png
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 app/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 35 additions & 18 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ import '@/styles/globals.css'
import { Analytics } from '@vercel/analytics/next'
import type { Metadata, Viewport } from 'next'
import type { ReactNode } from 'react'
import { themes } from '@/styles/colors'
import { fontsVariable } from '@/styles/fonts'

const SITE_URL = 'https://spargo.darkroom.engineering'
const APP_BASE_URL = 'https://spargo.darkroom.engineering'
const APP_NAME = 'Spargo'
const APP_DEFAULT_TITLE = 'Spargo'
const APP_TITLE_TEMPLATE = '%s — Spargo'
const APP_DESCRIPTION =
'Real-time GPU image dithering in the browser, via WebGL. Drop an image and scatter pixels to simulate tone and shade. By darkroom.engineering.'

export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
title: 'Spargo — GPU Image Dithering',
description:
'Real-time GPU image dithering in the browser, via WebGL. Drop an image and scatter pixels to simulate tone and shade. By Darkroom Engineering.',
applicationName: 'Spargo',
authors: [
{ name: 'Darkroom Engineering', url: 'https://darkroom.engineering' },
],
metadataBase: new URL(APP_BASE_URL),
applicationName: APP_NAME,
title: {
default: APP_DEFAULT_TITLE,
template: APP_TITLE_TEMPLATE,
},
description: APP_DESCRIPTION,
keywords: [
'dithering',
'webgl',
Expand All @@ -22,30 +28,41 @@ export const metadata: Metadata = {
'bayer',
'ordered dithering',
],
alternates: { canonical: '/' },
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: APP_DEFAULT_TITLE,
},
formatDetection: { telephone: false },
openGraph: {
type: 'website',
url: SITE_URL,
title: 'Spargo — GPU Image Dithering',
description:
'Real-time GPU image dithering in the browser, via WebGL. By Darkroom Engineering.',
siteName: 'Spargo',
siteName: APP_NAME,
title: { default: APP_DEFAULT_TITLE, template: APP_TITLE_TEMPLATE },
description: APP_DESCRIPTION,
url: APP_BASE_URL,
locale: 'en_US',
},
twitter: {
card: 'summary_large_image',
title: 'Spargo — GPU Image Dithering',
description: 'Real-time GPU image dithering in the browser, via WebGL.',
title: { default: APP_DEFAULT_TITLE, template: APP_TITLE_TEMPLATE },
description: APP_DESCRIPTION,
},
authors: [
{ name: 'darkroom.engineering', url: 'https://darkroom.engineering' },
],
}

export const viewport: Viewport = {
themeColor: '#0a0a0a',
themeColor: themes.dark.primary,
colorScheme: 'dark',
width: 'device-width',
initialScale: 1,
}

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<html lang="en" dir="ltr" className={fontsVariable} data-theme="dark">
<body>
{children}
<Analytics />
Expand Down
24 changes: 24 additions & 0 deletions app/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { MetadataRoute } from 'next'
import { themes } from '@/styles/colors'

export default function manifest(): MetadataRoute.Manifest {
return {
name: 'Spargo',
short_name: 'Spargo',
description: 'Real-time GPU image dithering in the browser, via WebGL.',
start_url: '/',
display: 'standalone',
background_color: themes.dark.primary,
theme_color: themes.dark.primary,
icons: [
{ src: '/icon.png', sizes: 'any', type: 'image/png' },
{ src: '/icon.png', sizes: '512x512', type: 'image/png' },
{
src: '/apple-icon.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any',
},
],
}
}
90 changes: 90 additions & 0 deletions app/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ImageResponse } from 'next/og'

export const alt = 'Spargo — GPU Image Dithering'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

const RED = '#e30613'
const MARK_SIZES = [380, 312, 244, 176, 108, 48]

export default function Image() {
return new ImageResponse(
<div
style={{
width: '100%',
height: '100%',
background: '#000000',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '90px',
fontFamily: 'sans-serif',
}}
>
{/* Layered red dither mark */}
<div
style={{
position: 'relative',
width: 380,
height: 380,
display: 'flex',
}}
>
{MARK_SIZES.map((s, i) => (
<div
key={s}
style={{
position: 'absolute',
top: (380 - s) / 2,
left: (380 - s) / 2,
width: s,
height: s,
background: RED,
opacity: 0.16 + i * 0.16,
}}
/>
))}
</div>

{/* Wordmark */}
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
gap: 14,
}}
>
<div
style={{
fontSize: 168,
fontWeight: 700,
color: RED,
letterSpacing: -4,
lineHeight: 1,
}}
>
SPARGO
</div>
<div style={{ fontSize: 30, color: RED, letterSpacing: 10 }}>
GPU DITHERING
</div>
</div>

{/* Footer */}
<div
style={{
position: 'absolute',
bottom: 44,
left: 90,
fontSize: 22,
color: RED,
letterSpacing: 6,
}}
>
DARKROOM.ENGINEERING
</div>
</div>,
size
)
}
8 changes: 8 additions & 0 deletions app/robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/' },
sitemap: 'https://spargo.darkroom.engineering/sitemap.xml',
}
}
11 changes: 11 additions & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://spargo.darkroom.engineering',
changeFrequency: 'monthly',
priority: 1,
},
]
}
1 change: 1 addition & 0 deletions app/twitter-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { alt, contentType, default, size } from './opengraph-image'
24 changes: 9 additions & 15 deletions components/controls/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@
import { Select } from '@base-ui/react/select'
import { useRef } from 'react'
import { RgbColorPicker } from 'react-colorful'
import {
configSchema,
DEFAULT_CONFIG,
DITHER_MODES,
type RGB,
useStore,
} from '@/lib/store'
import { configSchema, DITHER_MODES, type RGB, useStore } from '@/lib/store'
import { ColorField } from './color-field'
import { SliderField } from './slider-field'

Expand Down Expand Up @@ -46,8 +40,9 @@ export function Controls() {
}

function exportConfig() {
const { color: c } = useStore.getState()
const config = { gammaCorrection, granularity, color: c, opacity, mode }
const { gammaCorrection, granularity, color, opacity, mode } =
useStore.getState()
const config = { gammaCorrection, granularity, color, opacity, mode }
const blob = new Blob([JSON.stringify(config, null, 2)], {
type: 'application/json',
})
Expand Down Expand Up @@ -83,7 +78,9 @@ export function Controls() {
aria-label="Dithering controls"
>
<header className="flex items-baseline justify-between">
<h1 className="font-bold text-sm uppercase tracking-[0.2em]">spargo</h1>
<h1 className="font-bold font-display text-[var(--color-red)] text-lg uppercase tracking-[0.25em]">
spargo
</h1>
<span className="text-[10px] text-white/40">GPU dithering</span>
</header>

Expand Down Expand Up @@ -189,7 +186,7 @@ export function Controls() {
</button>
<button
type="button"
className="rounded-md border border-[var(--color-border)] px-2 py-1.5 hover:bg-white/10 data-[recording=true]:border-red-400 data-[recording=true]:text-red-300"
className="rounded-md border border-[var(--color-border)] px-2 py-1.5 hover:bg-white/10 data-[recording=true]:border-[var(--color-red)] data-[recording=true]:text-[var(--color-red)]"
data-recording={recording}
onClick={() =>
runAction(recording ? 'stopRecording' : 'startRecording')
Expand All @@ -214,10 +211,7 @@ export function Controls() {
<button
type="button"
className="col-span-2 rounded-md border border-[var(--color-border)] px-2 py-1.5 text-white/60 hover:bg-white/10"
onClick={() => {
resetConfig()
loadConfig(DEFAULT_CONFIG)
}}
onClick={resetConfig}
>
Reset
</button>
Expand Down
3 changes: 2 additions & 1 deletion lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export type DitheringConfig = z.infer<typeof configSchema>
export const DEFAULT_CONFIG: DitheringConfig = {
gammaCorrection: 1,
granularity: 1,
color: { r: 0, g: 0, b: 1 },
// Darkroom red (#e30613)
color: { r: 0.8902, g: 0.0235, b: 0.0745 },
opacity: 1,
mode: 'DOT_DIAGONAL_16x16',
}
Expand Down
34 changes: 34 additions & 0 deletions lib/styles/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Darkroom default palette (matches satus).
const colors = {
black: '#000000',
white: '#ffffff',
red: '#e30613',
} as const

const themeNames = ['light', 'dark', 'red'] as const
const colorNames = ['primary', 'secondary', 'contrast'] as const

const themes = {
light: {
primary: colors.white,
secondary: colors.black,
contrast: colors.red,
},
dark: {
primary: colors.black,
secondary: colors.white,
contrast: colors.red,
},
red: {
primary: colors.red,
secondary: colors.black,
contrast: colors.white,
},
} as const satisfies Themes

export { colors, themeNames, themes }

export type Themes = Record<
(typeof themeNames)[number],
Record<(typeof colorNames)[number], string>
>
23 changes: 23 additions & 0 deletions lib/styles/fonts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Oswald, Spline_Sans_Mono } from 'next/font/google'

// Darkroom default type pairing (matches satus): Oswald for display, Spline
// Sans Mono for UI/mono.
const display = Oswald({
weight: ['400', '700'],
subsets: ['latin'],
display: 'swap',
variable: '--next-font-display',
fallback: ['Arial Narrow', 'Arial', 'sans-serif'],
})

const mono = Spline_Sans_Mono({
weight: ['400', '500'],
subsets: ['latin'],
display: 'swap',
variable: '--next-font-mono',
fallback: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'monospace'],
})

const fonts = [display, mono]

export const fontsVariable = fonts.map((font) => font.variable).join(' ')
Loading
Loading