mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
feat(ui): add shared seeded paper gradients (#1288)
* feat(ui): add shared seeded paper gradients Centralize the Paper mesh and grain wrappers so React and Solid apps can reuse the same deterministic seed-based visuals without repeating shader config. Add a standalone demo surface and update existing consumers so the shared package is easier to validate and evolve. * fix(ui): resolve shared package from source Point the shared UI package exports at source files so Next builds do not depend on a prebuilt dist directory. Add Next transpilation for @openwork/ui in Landing and Den Web so monorepo and Vercel builds resolve the package consistently. --------- Co-authored-by: src-opn <src-opn@users.noreply.github.com>
This commit is contained in:
committed by
Benjamin Shafii
parent
d4592353e5
commit
c1e12a41fb
@@ -6,10 +6,13 @@
|
||||
"scripts": {
|
||||
"dev": "OPENWORK_DEV_MODE=1 vite",
|
||||
"dev:windows": "vite",
|
||||
"prebuild": "pnpm --dir ../../packages/ui build",
|
||||
"build": "vite build",
|
||||
"dev:web": "OPENWORK_DEV_MODE=1 vite",
|
||||
"prebuild:web": "pnpm --dir ../../packages/ui build",
|
||||
"build:web": "vite build",
|
||||
"preview": "vite preview",
|
||||
"pretypecheck": "pnpm --dir ../../packages/ui build",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"test:health": "node scripts/health.mjs",
|
||||
"test:mention-send": "node scripts/mention-send.mjs",
|
||||
@@ -32,6 +35,7 @@
|
||||
"bump:set": "node scripts/bump-version.mjs --set"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openwork/ui": "workspace:*",
|
||||
"@codemirror/commands": "^6.8.0",
|
||||
"@codemirror/lang-markdown": "^6.3.3",
|
||||
"@codemirror/language": "^6.11.0",
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paper-design/shaders-react": "0.0.71",
|
||||
"@vercel/blob": "^0.27.0",
|
||||
"botid": "^1.5.11",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
|
||||
12
apps/ui-demo/index.html
Normal file
12
apps/ui-demo/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenWork UI Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
apps/ui-demo/package.json
Normal file
24
apps/ui-demo/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@openwork/ui-demo",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "pnpm --dir ../../packages/ui build && vite --host 0.0.0.0 --port 3333 --strictPort",
|
||||
"build": "pnpm --dir ../../packages/ui build && vite build",
|
||||
"preview": "vite preview --host 0.0.0.0 --port 3333 --strictPort",
|
||||
"typecheck": "pnpm --dir ../../packages/ui build && tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openwork/ui": "workspace:*",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.12"
|
||||
}
|
||||
}
|
||||
216
apps/ui-demo/src/app.tsx
Normal file
216
apps/ui-demo/src/app.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import {
|
||||
PaperGrainGradient,
|
||||
PaperMeshGradient,
|
||||
getSeededPaperGrainGradientConfig,
|
||||
getSeededPaperMeshGradientConfig,
|
||||
} from "@openwork/ui/react"
|
||||
import { useMemo, useState } from "react"
|
||||
|
||||
const sampleIds = [
|
||||
"om_01kmhbscaze02vp04ykqa4tcsb",
|
||||
"om_01kmhbscazf4cjf1bssx6v9q9",
|
||||
"ow_01kmj2wc68r1zk4n8v7j6v1n2k",
|
||||
]
|
||||
|
||||
export function App() {
|
||||
const [seed, setSeed] = useState(sampleIds[0])
|
||||
const normalizedSeed = seed.trim() || sampleIds[0]
|
||||
const parsedSeed = parseTypeId(normalizedSeed)
|
||||
const meshConfig = useMemo(() => getSeededPaperMeshGradientConfig(normalizedSeed), [normalizedSeed])
|
||||
const grainConfig = useMemo(() => getSeededPaperGrainGradientConfig(normalizedSeed), [normalizedSeed])
|
||||
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<div className="ambient ambient-a" />
|
||||
<div className="ambient ambient-b" />
|
||||
|
||||
<section className="hero-card panel">
|
||||
<div className="hero-copy">
|
||||
<span className="eyebrow">OpenWork UI demo</span>
|
||||
<h1>Seeded Paper gradients on their own dev surface</h1>
|
||||
<p>
|
||||
Type a TypeID-like string, inspect the deterministic values derived from it, and preview
|
||||
the gradients that `@openwork/ui/react` will render anywhere else in the repo.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rule-card">
|
||||
<span className="eyebrow muted">Deterministic</span>
|
||||
<strong>Same seed, same result.</strong>
|
||||
<p>Useful for stable identity-driven art direction across apps.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="controls-grid">
|
||||
<div className="panel input-panel">
|
||||
<label className="eyebrow muted" htmlFor="seed-id">
|
||||
Seed id
|
||||
</label>
|
||||
<input
|
||||
id="seed-id"
|
||||
className="seed-input"
|
||||
type="text"
|
||||
value={seed}
|
||||
onChange={(event) => setSeed(event.target.value)}
|
||||
spellCheck={false}
|
||||
/>
|
||||
|
||||
<div className="sample-list">
|
||||
{sampleIds.map((sampleId) => (
|
||||
<button
|
||||
key={sampleId}
|
||||
type="button"
|
||||
className={sampleId === normalizedSeed ? "sample-chip active" : "sample-chip"}
|
||||
onClick={() => setSeed(sampleId)}
|
||||
>
|
||||
{sampleId}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="panel seed-meta-grid">
|
||||
<SeedMeta label="prefix" value={parsedSeed.prefix ?? "-"} />
|
||||
<SeedMeta label="suffix" value={parsedSeed.suffix ?? "-"} />
|
||||
<SeedMeta label="suffix first 5" value={parsedSeed.suffixAnchor ?? "-"} />
|
||||
<SeedMeta label="suffix tail" value={parsedSeed.suffixTail ?? "-"} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="preview-grid">
|
||||
<GradientCard
|
||||
title="Mesh gradient"
|
||||
subtitle="Shared mesh defaults plus seeded color and motion variation"
|
||||
colors={meshConfig.colors}
|
||||
config={meshConfig}
|
||||
surface={<PaperMeshGradient seed={normalizedSeed} className="gradient-fill" />}
|
||||
/>
|
||||
|
||||
<GradientCard
|
||||
title="Grain gradient"
|
||||
subtitle="Shared grain defaults plus seeded background, shape, and values"
|
||||
colors={[grainConfig.colorBack, ...grainConfig.colors]}
|
||||
config={grainConfig}
|
||||
surface={<PaperGrainGradient seed={normalizedSeed} className="gradient-fill" />}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="footer-grid">
|
||||
<div className="panel">
|
||||
<span className="eyebrow muted">Determinism check</span>
|
||||
<div className="mini-grid">
|
||||
<MiniPreview title="Mesh A">
|
||||
<PaperMeshGradient seed={normalizedSeed} className="gradient-fill" />
|
||||
</MiniPreview>
|
||||
<MiniPreview title="Mesh B">
|
||||
<PaperMeshGradient seed={normalizedSeed} className="gradient-fill" />
|
||||
</MiniPreview>
|
||||
</div>
|
||||
<p className="support-copy">
|
||||
These two cards use the same seed and should always match.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="panel code-panel">
|
||||
<span className="eyebrow muted">Import paths</span>
|
||||
<div className="pill-stack">
|
||||
<code className="import-pill">@openwork/ui/react</code>
|
||||
<code className="import-pill">@openwork/ui/solid</code>
|
||||
</div>
|
||||
<pre>{`import { PaperMeshGradient, PaperGrainGradient } from "@openwork/ui/react"
|
||||
|
||||
<PaperMeshGradient seed="${normalizedSeed}" />
|
||||
<PaperGrainGradient seed="${normalizedSeed}" />`}</pre>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function GradientCard({
|
||||
title,
|
||||
subtitle,
|
||||
colors,
|
||||
config,
|
||||
surface,
|
||||
}: {
|
||||
title: string
|
||||
subtitle: string
|
||||
colors: string[]
|
||||
config: Record<string, unknown>
|
||||
surface: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<article className="panel preview-card">
|
||||
<div className="gradient-surface">
|
||||
{surface}
|
||||
<div className="surface-overlay" />
|
||||
<div className="surface-copy">
|
||||
<span className="eyebrow on-dark">@openwork/ui/react</span>
|
||||
<h2>{title}</h2>
|
||||
<p>{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="details-stack">
|
||||
<div>
|
||||
<span className="eyebrow muted">Colors</span>
|
||||
<div className="swatch-list">
|
||||
{colors.map((color) => (
|
||||
<div key={color} className="swatch-pill">
|
||||
<span className="swatch-dot" style={{ backgroundColor: color }} />
|
||||
<code>{color}</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="eyebrow muted">Calculated values</span>
|
||||
<pre>{JSON.stringify(config, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function MiniPreview({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<span className="eyebrow muted">{title}</span>
|
||||
<div className="mini-surface">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SeedMeta({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="seed-meta-card">
|
||||
<span className="eyebrow muted">{label}</span>
|
||||
<code>{value}</code>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function parseTypeId(value: string) {
|
||||
const separatorIndex = value.indexOf("_")
|
||||
|
||||
if (separatorIndex === -1) {
|
||||
return {
|
||||
prefix: null,
|
||||
suffix: value,
|
||||
suffixAnchor: value.slice(0, 5) || null,
|
||||
suffixTail: value.slice(5) || null,
|
||||
}
|
||||
}
|
||||
|
||||
const prefix = value.slice(0, separatorIndex) || null
|
||||
const suffix = value.slice(separatorIndex + 1) || null
|
||||
|
||||
return {
|
||||
prefix,
|
||||
suffix,
|
||||
suffixAnchor: suffix?.slice(0, 5) || null,
|
||||
suffixTail: suffix?.slice(5) || null,
|
||||
}
|
||||
}
|
||||
10
apps/ui-demo/src/main.tsx
Normal file
10
apps/ui-demo/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { App } from "./app"
|
||||
import "./styles.css"
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
343
apps/ui-demo/src/styles.css
Normal file
343
apps/ui-demo/src/styles.css
Normal file
@@ -0,0 +1,343 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family: "IBM Plex Sans", "Inter", system-ui, sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(57, 181, 74, 0.16), transparent 28%),
|
||||
radial-gradient(circle at top right, rgba(39, 98, 255, 0.14), transparent 30%),
|
||||
linear-gradient(180deg, #f6f1e8 0%, #efe7d7 100%);
|
||||
color: #1f2c2b;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
code,
|
||||
pre,
|
||||
.sample-chip,
|
||||
.seed-input {
|
||||
font-family: "IBM Plex Mono", "SFMono-Regular", ui-monospace, monospace;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
padding: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ambient {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
border-radius: 999px;
|
||||
filter: blur(70px);
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.ambient-a {
|
||||
top: -120px;
|
||||
left: -80px;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
background: rgba(76, 175, 80, 0.22);
|
||||
}
|
||||
|
||||
.ambient-b {
|
||||
right: -80px;
|
||||
bottom: 80px;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
border: 1px solid rgba(24, 30, 28, 0.08);
|
||||
background: rgba(255, 251, 245, 0.82);
|
||||
backdrop-filter: blur(18px);
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 24px 80px -48px rgba(29, 24, 17, 0.45);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(260px, 0.8fr);
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.hero-copy h1 {
|
||||
margin: 10px 0 0;
|
||||
font-size: clamp(2.4rem, 6vw, 4.5rem);
|
||||
line-height: 0.92;
|
||||
letter-spacing: -0.08em;
|
||||
}
|
||||
|
||||
.hero-copy p,
|
||||
.rule-card p,
|
||||
.support-copy,
|
||||
.surface-copy p {
|
||||
margin: 0;
|
||||
color: #516160;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.hero-copy p {
|
||||
margin-top: 18px;
|
||||
max-width: 62ch;
|
||||
}
|
||||
|
||||
.rule-card {
|
||||
align-self: end;
|
||||
padding: 20px;
|
||||
border-radius: 24px;
|
||||
background: #16201f;
|
||||
color: #f0f7f3;
|
||||
}
|
||||
|
||||
.rule-card p {
|
||||
margin-top: 8px;
|
||||
color: rgba(240, 247, 243, 0.74);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-block;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #6d7a79;
|
||||
}
|
||||
|
||||
.on-dark {
|
||||
color: rgba(255, 255, 255, 0.68);
|
||||
}
|
||||
|
||||
.controls-grid,
|
||||
.footer-grid {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.95fr);
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.input-panel,
|
||||
.seed-meta-grid,
|
||||
.code-panel {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.seed-input {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 15px 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(22, 32, 31, 0.12);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #182321;
|
||||
font-size: 0.92rem;
|
||||
outline: none;
|
||||
transition: border-color 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.seed-input:focus {
|
||||
border-color: #287d75;
|
||||
box-shadow: 0 0 0 5px rgba(40, 125, 117, 0.12);
|
||||
}
|
||||
|
||||
.sample-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.sample-chip {
|
||||
border: 1px solid rgba(22, 32, 31, 0.1);
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
color: #425251;
|
||||
border-radius: 999px;
|
||||
padding: 9px 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sample-chip.active {
|
||||
background: #1f6978;
|
||||
color: white;
|
||||
border-color: #1f6978;
|
||||
}
|
||||
|
||||
.seed-meta-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
background: #16201f;
|
||||
}
|
||||
|
||||
.seed-meta-card {
|
||||
padding: 18px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.seed-meta-card code {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #f4fbf7;
|
||||
word-break: break-all;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.preview-grid {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradient-surface {
|
||||
position: relative;
|
||||
min-height: 340px;
|
||||
background: #101818;
|
||||
}
|
||||
|
||||
.gradient-fill {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.surface-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(10, 18, 18, 0.08), rgba(10, 18, 18, 0.34));
|
||||
}
|
||||
|
||||
.surface-copy {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
padding: 24px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.surface-copy h2 {
|
||||
margin: 10px 0 0;
|
||||
font-size: 2rem;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.surface-copy p {
|
||||
margin-top: 10px;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.details-stack {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.swatch-list,
|
||||
.pill-stack {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.swatch-pill,
|
||||
.import-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(22, 32, 31, 0.08);
|
||||
background: rgba(248, 243, 235, 0.9);
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.swatch-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
margin: 14px 0 0;
|
||||
border-radius: 22px;
|
||||
background: #16201f;
|
||||
color: #dcebe4;
|
||||
padding: 18px;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.mini-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mini-surface {
|
||||
position: relative;
|
||||
min-height: 180px;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: 22px;
|
||||
background: #111918;
|
||||
}
|
||||
|
||||
.support-copy {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.hero-card,
|
||||
.controls-grid,
|
||||
.preview-grid,
|
||||
.footer-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app-shell {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.input-panel,
|
||||
.seed-meta-grid,
|
||||
.code-panel,
|
||||
.details-stack {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.seed-meta-grid,
|
||||
.mini-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
17
apps/ui-demo/tsconfig.json
Normal file
17
apps/ui-demo/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
}
|
||||
19
apps/ui-demo/vite.config.ts
Normal file
19
apps/ui-demo/vite.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from "vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 3333,
|
||||
strictPort: true,
|
||||
},
|
||||
preview: {
|
||||
host: "0.0.0.0",
|
||||
port: 3333,
|
||||
strictPort: true,
|
||||
},
|
||||
build: {
|
||||
target: "es2022",
|
||||
},
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Dithering, MeshGradient } from "@paper-design/shaders-react";
|
||||
import { PaperMeshGradient } from "@openwork/ui/react";
|
||||
import { Dithering } from "@paper-design/shaders-react";
|
||||
import { ArrowRight, CheckCircle2 } from "lucide-react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -184,7 +185,7 @@ export function AuthScreen() {
|
||||
colorFront="#FEFEFE"
|
||||
style={{ backgroundColor: "#142033", width: "100%", height: "100%" }}
|
||||
>
|
||||
<MeshGradient
|
||||
<PaperMeshGradient
|
||||
speed={0.1}
|
||||
distortion={0.8}
|
||||
swirl={0.1}
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
Plus,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { Dithering, MeshGradient } from "@paper-design/shaders-react";
|
||||
import { PaperMeshGradient } from "@openwork/ui/react";
|
||||
import { Dithering } from "@paper-design/shaders-react";
|
||||
import {
|
||||
OPENWORK_APP_CONNECT_BASE_URL,
|
||||
buildOpenworkAppConnectUrl,
|
||||
@@ -406,7 +407,7 @@ export function BackgroundAgentsScreen() {
|
||||
colorFront="#FEFEFE"
|
||||
style={{ backgroundColor: "#23301C", width: "100%", height: "100%" }}
|
||||
>
|
||||
<MeshGradient
|
||||
<PaperMeshGradient
|
||||
speed={0}
|
||||
distortion={0.8}
|
||||
swirl={0.1}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Cpu } from "lucide-react";
|
||||
import { Dithering, MeshGradient } from "@paper-design/shaders-react";
|
||||
import { PaperMeshGradient } from "@openwork/ui/react";
|
||||
import { Dithering } from "@paper-design/shaders-react";
|
||||
|
||||
const comingSoonItems = [
|
||||
"Standardize provider access across your team.",
|
||||
@@ -25,7 +26,7 @@ export function CustomLlmProvidersScreen() {
|
||||
colorFront="#FEFEFE"
|
||||
style={{ backgroundColor: "#1C2A30", width: "100%", height: "100%" }}
|
||||
>
|
||||
<MeshGradient
|
||||
<PaperMeshGradient
|
||||
speed={0.1}
|
||||
distortion={0.8}
|
||||
swirl={0.1}
|
||||
|
||||
@@ -4,6 +4,7 @@ const path = require("path");
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
skipTrailingSlashRedirect: true,
|
||||
transpilePackages: ["@openwork/ui"],
|
||||
outputFileTracingRoot: path.join(__dirname, "../../.."),
|
||||
};
|
||||
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
"scripts": {
|
||||
"dev": "OPENWORK_DEV_MODE=1 NEXT_PUBLIC_POSTHOG_KEY= NEXT_PUBLIC_POSTHOG_API_KEY= next dev --hostname 0.0.0.0 --port 3005",
|
||||
"dev:local": "sh -lc 'OPENWORK_DEV_MODE=1 NEXT_PUBLIC_POSTHOG_KEY= NEXT_PUBLIC_POSTHOG_API_KEY= next dev --hostname 0.0.0.0 --port ${DEN_WEB_PORT:-3005}'",
|
||||
"prebuild": "pnpm --dir ../../../packages/ui build",
|
||||
"build": "next build",
|
||||
"start": "next start --hostname 0.0.0.0 --port 3005",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paper-design/shaders-react": "0.0.71",
|
||||
"@openwork/ui": "workspace:*",
|
||||
"@paper-design/shaders-react": "0.0.72",
|
||||
"lucide-react": "^0.577.0",
|
||||
"next": "16.2.1",
|
||||
"react": "19.2.4",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { GrainGradient } from "@paper-design/shaders-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { PaperGrainGradient } from "@openwork/ui/react";
|
||||
|
||||
type Props = {
|
||||
seed?: string;
|
||||
colors: string[];
|
||||
colorBack: string;
|
||||
softness: number;
|
||||
@@ -15,34 +15,10 @@ type Props = {
|
||||
};
|
||||
|
||||
export function ResponsiveGrain(props: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
setDimensions({
|
||||
width: entry.contentRect.width,
|
||||
height: entry.contentRect.height
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
<PaperGrainGradient
|
||||
className={`absolute inset-0 overflow-hidden ${props.className || ""}`}
|
||||
>
|
||||
{dimensions.width > 0 && dimensions.height > 0 ? (
|
||||
<GrainGradient
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
seed={props.seed}
|
||||
colors={props.colors}
|
||||
colorBack={props.colorBack}
|
||||
softness={props.softness}
|
||||
@@ -51,7 +27,5 @@ export function ResponsiveGrain(props: Props) {
|
||||
shape={props.shape}
|
||||
speed={props.speed}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const mintlifyOrigin = "https://differentai.mintlify.dev";
|
||||
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ["@openwork/ui"],
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "OPENWORK_DEV_MODE=1 next dev --hostname 0.0.0.0",
|
||||
"prebuild": "pnpm --dir ../../../packages/ui build",
|
||||
"build": "next build",
|
||||
"start": "next start --hostname 0.0.0.0",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paper-design/shaders-react": "0.0.71",
|
||||
"@openwork/ui": "workspace:*",
|
||||
"botid": "^1.5.11",
|
||||
"framer-motion": "^12.35.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"dev:windows": ".\\scripts\\dev-windows.cmd",
|
||||
"dev:windows:x64": ".\\scripts\\dev-windows.cmd x64",
|
||||
"dev:ui": "OPENWORK_DEV_MODE=1 pnpm --filter @openwork/app dev",
|
||||
"dev:ui-demo": "pnpm --filter @openwork/ui-demo dev",
|
||||
"dev:story": "OPENWORK_DEV_MODE=1 pnpm --filter @openwork/story-book dev",
|
||||
"dev:web": "OPENWORK_DEV_MODE=1 pnpm --filter @openwork-ee/den-web dev",
|
||||
"dev:web-local": "pnpm dev:den-local",
|
||||
|
||||
39
packages/ui/README.md
Normal file
39
packages/ui/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# @openwork/ui
|
||||
|
||||
Shared UI primitives for OpenWork apps.
|
||||
|
||||
This package intentionally ships two framework-specific entrypoints:
|
||||
|
||||
- `@openwork/ui/react` for React apps like `ee/apps/den-web`
|
||||
- `@openwork/ui/solid` for Solid apps like `apps/app`
|
||||
|
||||
The public API should stay aligned across both entrypoints. If you add a new component, add both implementations in the same task unless there is a documented blocker.
|
||||
|
||||
## Paper components
|
||||
|
||||
The first shared components live under the `paper` namespace and wrap Paper Design shaders with OpenWork-specific defaults and deterministic seed support.
|
||||
|
||||
Current components:
|
||||
|
||||
- `PaperMeshGradient`
|
||||
- `PaperGrainGradient`
|
||||
|
||||
Both accept a `seed` prop. Pass a TypeID-like string such as `om_01kmhbscaze02vp04ykqa4tcsb` and the component will deterministically derive colors and shader params from it. The same seed always produces the same result.
|
||||
|
||||
Explicit props still work and override the seeded values, so the merge order is:
|
||||
|
||||
1. OpenWork defaults
|
||||
2. Seed-derived values from `seed`
|
||||
3. Explicit props passed by the caller
|
||||
|
||||
## Layout convention
|
||||
|
||||
These components default to `fill={true}`, which means they render at `width: 100%` and `height: 100%`. Put them inside a sized container and they will fill it without needing manual width or height props.
|
||||
|
||||
## Agent notes
|
||||
|
||||
- Shared seed logic lives in `src/common/paper.ts`
|
||||
- React wrappers live in `src/react/paper/*`
|
||||
- Solid wrappers live in `src/solid/paper/*`
|
||||
- Keep the framework prop names aligned unless there is a hard runtime mismatch
|
||||
- Prefer extending the existing seed helpers instead of inventing per-app one-off shader configs
|
||||
38
packages/ui/package.json
Normal file
38
packages/ui/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@openwork/ui",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./react": {
|
||||
"types": "./src/react/index.ts",
|
||||
"development": "./src/react/index.ts",
|
||||
"default": "./src/react/index.ts"
|
||||
},
|
||||
"./solid": {
|
||||
"types": "./src/solid/index.ts",
|
||||
"development": "./src/solid/index.ts",
|
||||
"default": "./src/solid/index.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paper-design/shaders": "0.0.72",
|
||||
"@paper-design/shaders-react": "0.0.72"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19",
|
||||
"solid-js": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.14",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
416
packages/ui/src/common/paper.ts
Normal file
416
packages/ui/src/common/paper.ts
Normal file
@@ -0,0 +1,416 @@
|
||||
import type {
|
||||
GrainGradientParams,
|
||||
GrainGradientShape,
|
||||
MeshGradientParams,
|
||||
} from "@paper-design/shaders"
|
||||
|
||||
export type PaperMeshGradientConfig = Required<
|
||||
Pick<
|
||||
MeshGradientParams,
|
||||
"colors" | "distortion" | "swirl" | "grainMixer" | "grainOverlay" | "speed" | "frame"
|
||||
>
|
||||
>
|
||||
|
||||
export type PaperGrainGradientConfig = Required<
|
||||
Pick<
|
||||
GrainGradientParams,
|
||||
"colorBack" | "colors" | "softness" | "intensity" | "noise" | "shape" | "speed" | "frame"
|
||||
>
|
||||
>
|
||||
|
||||
export type SeededPaperOption = {
|
||||
seed?: string
|
||||
}
|
||||
|
||||
export const paperMeshGradientDefaults: PaperMeshGradientConfig = {
|
||||
colors: ["#e0eaff", "#241d9a", "#f75092", "#9f50d3"],
|
||||
distortion: 0.8,
|
||||
swirl: 0.1,
|
||||
grainMixer: 0,
|
||||
grainOverlay: 0,
|
||||
speed: 0.1,
|
||||
frame: 0,
|
||||
}
|
||||
|
||||
export const paperGrainGradientDefaults: PaperGrainGradientConfig = {
|
||||
colors: ["#7300ff", "#eba8ff", "#00bfff", "#2b00ff"],
|
||||
colorBack: "#000000",
|
||||
softness: 0.5,
|
||||
intensity: 0.5,
|
||||
noise: 0.25,
|
||||
shape: "ripple",
|
||||
speed: 0.4,
|
||||
frame: 0,
|
||||
}
|
||||
|
||||
const grainShapes: GrainGradientShape[] = [
|
||||
"corners",
|
||||
"wave",
|
||||
"dots",
|
||||
"truchet",
|
||||
"ripple",
|
||||
"blob",
|
||||
"sphere",
|
||||
]
|
||||
|
||||
const meshPaletteFamilies = [
|
||||
["#e0eaff", "#241d9a", "#f75092", "#9f50d3"],
|
||||
["#ddfff5", "#006c67", "#35d8c0", "#8cff7a"],
|
||||
["#ffe5c2", "#8a2500", "#ff7b39", "#ffd166"],
|
||||
["#f5f7ff", "#0d1b52", "#3f8cff", "#00c2ff"],
|
||||
["#fff2f2", "#6f1237", "#ff4d6d", "#ffb703"],
|
||||
["#f0ffe1", "#254d00", "#8cc63f", "#00a76f"],
|
||||
["#f5edff", "#44206b", "#b5179e", "#7209b7"],
|
||||
["#f4f1ea", "#3a2f1f", "#927c55", "#d0c2a8"],
|
||||
]
|
||||
|
||||
const grainPaletteFamilies = [
|
||||
["#7300ff", "#eba8ff", "#00bfff", "#2b00ff"],
|
||||
["#0df2c1", "#0b7cff", "#74efff", "#1a2cff"],
|
||||
["#ff7a18", "#ffd166", "#ff4d6d", "#5f0f40"],
|
||||
["#8dff6a", "#1f7a1f", "#d7ff70", "#00c48c"],
|
||||
["#f6a6ff", "#7027c9", "#ff66c4", "#20115b"],
|
||||
["#b9ecff", "#006494", "#00a6a6", "#072ac8"],
|
||||
["#f7f0d6", "#8c5e34", "#d68c45", "#4e342e"],
|
||||
["#ffd9f5", "#ff006e", "#8338ec", "#3a0ca3"],
|
||||
]
|
||||
|
||||
const paletteModes = [
|
||||
{
|
||||
hueOffsets: [0, 22, 182, 238],
|
||||
saturations: [0.92, 0.7, 0.84, 0.74],
|
||||
lightnesses: [0.82, 0.28, 0.6, 0.5],
|
||||
},
|
||||
{
|
||||
hueOffsets: [0, 118, 242, 304],
|
||||
saturations: [0.88, 0.76, 0.82, 0.7],
|
||||
lightnesses: [0.8, 0.42, 0.58, 0.48],
|
||||
},
|
||||
{
|
||||
hueOffsets: [0, 44, 156, 214],
|
||||
saturations: [0.94, 0.78, 0.86, 0.72],
|
||||
lightnesses: [0.78, 0.4, 0.6, 0.46],
|
||||
},
|
||||
{
|
||||
hueOffsets: [0, 76, 184, 326],
|
||||
saturations: [0.86, 0.8, 0.78, 0.76],
|
||||
lightnesses: [0.82, 0.52, 0.42, 0.58],
|
||||
},
|
||||
{
|
||||
hueOffsets: [0, 140, 196, 224],
|
||||
saturations: [0.84, 0.7, 0.76, 0.88],
|
||||
lightnesses: [0.86, 0.46, 0.36, 0.54],
|
||||
},
|
||||
{
|
||||
hueOffsets: [0, 162, 212, 342],
|
||||
saturations: [0.9, 0.72, 0.8, 0.82],
|
||||
lightnesses: [0.8, 0.38, 0.52, 0.56],
|
||||
},
|
||||
]
|
||||
|
||||
type MeshGradientOverrides = SeededPaperOption & Partial<PaperMeshGradientConfig>
|
||||
type GrainGradientOverrides = SeededPaperOption & Partial<PaperGrainGradientConfig>
|
||||
|
||||
export function getSeededPaperMeshGradientConfig(seed: string): PaperMeshGradientConfig {
|
||||
const random = createRandom(seed, "mesh")
|
||||
|
||||
return {
|
||||
colors: createSeededPalette(paperMeshGradientDefaults.colors, seed, "mesh-colors", {
|
||||
families: meshPaletteFamilies,
|
||||
hueShift: 42,
|
||||
saturationShift: 0.18,
|
||||
lightnessShift: 0.14,
|
||||
baseBlend: [0.08, 0.2],
|
||||
}),
|
||||
distortion: roundTo(clamp(0.58 + random() * 0.32, 0, 1), 3),
|
||||
swirl: roundTo(clamp(0.03 + random() * 0.28, 0, 1), 3),
|
||||
grainMixer: roundTo(clamp(random() * 0.18, 0, 1), 3),
|
||||
grainOverlay: roundTo(clamp(random() * 0.12, 0, 1), 3),
|
||||
speed: roundTo(0.05 + random() * 0.11, 3),
|
||||
frame: Math.round(random() * 240000),
|
||||
}
|
||||
}
|
||||
|
||||
export function getSeededPaperGrainGradientConfig(seed: string): PaperGrainGradientConfig {
|
||||
const random = createRandom(seed, "grain")
|
||||
const colors = createSeededPalette(paperGrainGradientDefaults.colors, seed, "grain-colors", {
|
||||
families: grainPaletteFamilies,
|
||||
hueShift: 58,
|
||||
saturationShift: 0.22,
|
||||
lightnessShift: 0.18,
|
||||
baseBlend: [0.04, 0.14],
|
||||
})
|
||||
const anchorColor = colors[Math.floor(random() * colors.length)] ?? colors[0]
|
||||
|
||||
return {
|
||||
colors,
|
||||
colorBack: createSeededBackground(anchorColor, seed, "grain-background"),
|
||||
softness: roundTo(clamp(0.22 + random() * 0.56, 0, 1), 3),
|
||||
intensity: roundTo(clamp(0.2 + random() * 0.6, 0, 1), 3),
|
||||
noise: roundTo(clamp(0.12 + random() * 0.34, 0, 1), 3),
|
||||
shape: grainShapes[Math.floor(random() * grainShapes.length)] ?? paperGrainGradientDefaults.shape,
|
||||
speed: roundTo(0.2 + random() * 0.6, 3),
|
||||
frame: Math.round(random() * 320000),
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePaperMeshGradientConfig(
|
||||
options: MeshGradientOverrides = {},
|
||||
): PaperMeshGradientConfig {
|
||||
const seeded = options.seed ? getSeededPaperMeshGradientConfig(options.seed) : paperMeshGradientDefaults
|
||||
|
||||
return {
|
||||
colors: options.colors ?? seeded.colors,
|
||||
distortion: options.distortion ?? seeded.distortion,
|
||||
swirl: options.swirl ?? seeded.swirl,
|
||||
grainMixer: options.grainMixer ?? seeded.grainMixer,
|
||||
grainOverlay: options.grainOverlay ?? seeded.grainOverlay,
|
||||
speed: options.speed ?? seeded.speed,
|
||||
frame: options.frame ?? seeded.frame,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolvePaperGrainGradientConfig(
|
||||
options: GrainGradientOverrides = {},
|
||||
): PaperGrainGradientConfig {
|
||||
const seeded = options.seed ? getSeededPaperGrainGradientConfig(options.seed) : paperGrainGradientDefaults
|
||||
|
||||
return {
|
||||
colors: options.colors ?? seeded.colors,
|
||||
colorBack: options.colorBack ?? seeded.colorBack,
|
||||
softness: options.softness ?? seeded.softness,
|
||||
intensity: options.intensity ?? seeded.intensity,
|
||||
noise: options.noise ?? seeded.noise,
|
||||
shape: options.shape ?? seeded.shape,
|
||||
speed: options.speed ?? seeded.speed,
|
||||
frame: options.frame ?? seeded.frame,
|
||||
}
|
||||
}
|
||||
|
||||
function buildSeedSource(seed: string) {
|
||||
const trimmedSeed = seed.trim()
|
||||
const separatorIndex = trimmedSeed.indexOf("_")
|
||||
|
||||
if (separatorIndex === -1) {
|
||||
return trimmedSeed
|
||||
}
|
||||
|
||||
const prefix = trimmedSeed.slice(0, separatorIndex)
|
||||
const suffix = trimmedSeed.slice(separatorIndex + 1)
|
||||
const suffixTail = suffix.slice(5) || suffix
|
||||
|
||||
return `${trimmedSeed}|${prefix}|${suffix}|${suffixTail}`
|
||||
}
|
||||
|
||||
function createSeededPalette(
|
||||
baseColors: string[],
|
||||
seed: string,
|
||||
namespace: string,
|
||||
options: {
|
||||
families: string[][]
|
||||
hueShift: number
|
||||
saturationShift: number
|
||||
lightnessShift: number
|
||||
baseBlend: [number, number]
|
||||
},
|
||||
) {
|
||||
const familyRandom = createRandom(seed, `${namespace}:family`)
|
||||
const primaryIndex = Math.floor(familyRandom() * options.families.length)
|
||||
const secondaryOffset = 1 + Math.floor(familyRandom() * (options.families.length - 1))
|
||||
const secondaryIndex = (primaryIndex + secondaryOffset) % options.families.length
|
||||
const primary = options.families[primaryIndex] ?? baseColors
|
||||
const secondary = options.families[secondaryIndex] ?? [...baseColors].reverse()
|
||||
const primaryShift = Math.floor(familyRandom() * primary.length)
|
||||
const secondaryShift = Math.floor(familyRandom() * secondary.length)
|
||||
const paletteMode = paletteModes[Math.floor(familyRandom() * paletteModes.length)] ?? paletteModes[0]
|
||||
const baseHue = familyRandom() * 360
|
||||
|
||||
return baseColors.map((color, index) => {
|
||||
const random = createRandom(seed, `${namespace}:${index}`)
|
||||
const primaryColor = primary[(index + primaryShift) % primary.length] ?? color
|
||||
const secondaryColor = secondary[(index + secondaryShift) % secondary.length] ?? primaryColor
|
||||
const proceduralColor = hslToHex(
|
||||
(baseHue + paletteMode.hueOffsets[index % paletteMode.hueOffsets.length] + (random() * 2 - 1) * 18 + 360) % 360,
|
||||
clamp(paletteMode.saturations[index % paletteMode.saturations.length] + (random() * 2 - 1) * 0.08, 0, 1),
|
||||
clamp(paletteMode.lightnesses[index % paletteMode.lightnesses.length] + (random() * 2 - 1) * 0.08, 0, 1),
|
||||
)
|
||||
const mixedFamilyColor = mixHexColors(primaryColor, secondaryColor, 0.18 + random() * 0.64)
|
||||
const remixedFamilyColor = mixHexColors(
|
||||
mixedFamilyColor,
|
||||
primary[(index + secondaryShift + 1) % primary.length] ?? mixedFamilyColor,
|
||||
random() * 0.32,
|
||||
)
|
||||
const proceduralFamilyColor = mixHexColors(proceduralColor, remixedFamilyColor, 0.22 + random() * 0.34)
|
||||
const [minBaseBlend, maxBaseBlend] = options.baseBlend
|
||||
const blendedBaseColor = mixHexColors(
|
||||
proceduralFamilyColor,
|
||||
color,
|
||||
minBaseBlend + random() * (maxBaseBlend - minBaseBlend),
|
||||
)
|
||||
|
||||
return adjustHexColor(blendedBaseColor, {
|
||||
hueShift: (random() * 2 - 1) * options.hueShift + (random() * 2 - 1) * 14,
|
||||
saturationShift: (random() * 2 - 1) * options.saturationShift + 0.06,
|
||||
lightnessShift: (random() * 2 - 1) * options.lightnessShift,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function createSeededBackground(baseColor: string, seed: string, namespace: string) {
|
||||
const [red, green, blue] = hexToRgb(baseColor)
|
||||
const [hue] = rgbToHsl(red, green, blue)
|
||||
const random = createRandom(seed, namespace)
|
||||
|
||||
return hslToHex(
|
||||
hue,
|
||||
clamp(0.18 + random() * 0.18, 0, 1),
|
||||
clamp(0.03 + random() * 0.09, 0, 1),
|
||||
)
|
||||
}
|
||||
|
||||
function adjustHexColor(
|
||||
hex: string,
|
||||
adjustments: { hueShift: number; saturationShift: number; lightnessShift: number },
|
||||
) {
|
||||
const [red, green, blue] = hexToRgb(hex)
|
||||
const [hue, saturation, lightness] = rgbToHsl(red, green, blue)
|
||||
|
||||
return hslToHex(
|
||||
(hue + adjustments.hueShift + 360) % 360,
|
||||
clamp(saturation + adjustments.saturationShift, 0, 1),
|
||||
clamp(lightness + adjustments.lightnessShift, 0, 1),
|
||||
)
|
||||
}
|
||||
|
||||
function mixHexColors(colorA: string, colorB: string, amount: number) {
|
||||
const [redA, greenA, blueA] = hexToRgb(colorA)
|
||||
const [redB, greenB, blueB] = hexToRgb(colorB)
|
||||
const mixAmount = clamp(amount, 0, 1)
|
||||
|
||||
return rgbToHex(
|
||||
Math.round(redA + (redB - redA) * mixAmount),
|
||||
Math.round(greenA + (greenB - greenA) * mixAmount),
|
||||
Math.round(blueA + (blueB - blueA) * mixAmount),
|
||||
)
|
||||
}
|
||||
|
||||
function createRandom(seed: string, namespace: string) {
|
||||
return mulberry32(hashString(`${buildSeedSource(seed)}::${namespace}`))
|
||||
}
|
||||
|
||||
function hashString(input: string) {
|
||||
let hash = 2166136261
|
||||
|
||||
for (let index = 0; index < input.length; index += 1) {
|
||||
hash ^= input.charCodeAt(index)
|
||||
hash = Math.imul(hash, 16777619)
|
||||
}
|
||||
|
||||
return hash >>> 0
|
||||
}
|
||||
|
||||
function mulberry32(seed: number) {
|
||||
return function nextRandom() {
|
||||
let value = seed += 0x6d2b79f5
|
||||
value = Math.imul(value ^ (value >>> 15), value | 1)
|
||||
value ^= value + Math.imul(value ^ (value >>> 7), value | 61)
|
||||
return ((value ^ (value >>> 14)) >>> 0) / 4294967296
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
|
||||
function roundTo(value: number, precision: number) {
|
||||
const power = 10 ** precision
|
||||
return Math.round(value * power) / power
|
||||
}
|
||||
|
||||
function hexToRgb(hex: string): [number, number, number] {
|
||||
const normalized = hex.replace(/^#/, "")
|
||||
const expanded = normalized.length === 3
|
||||
? normalized.split("").map((part) => `${part}${part}`).join("")
|
||||
: normalized
|
||||
|
||||
if (expanded.length !== 6) {
|
||||
throw new Error(`Unsupported hex color: ${hex}`)
|
||||
}
|
||||
|
||||
const value = Number.parseInt(expanded, 16)
|
||||
|
||||
return [
|
||||
(value >> 16) & 255,
|
||||
(value >> 8) & 255,
|
||||
value & 255,
|
||||
]
|
||||
}
|
||||
|
||||
function rgbToHsl(red: number, green: number, blue: number): [number, number, number] {
|
||||
const normalizedRed = red / 255
|
||||
const normalizedGreen = green / 255
|
||||
const normalizedBlue = blue / 255
|
||||
const max = Math.max(normalizedRed, normalizedGreen, normalizedBlue)
|
||||
const min = Math.min(normalizedRed, normalizedGreen, normalizedBlue)
|
||||
const lightness = (max + min) / 2
|
||||
|
||||
if (max === min) {
|
||||
return [0, 0, lightness]
|
||||
}
|
||||
|
||||
const delta = max - min
|
||||
const saturation = lightness > 0.5
|
||||
? delta / (2 - max - min)
|
||||
: delta / (max + min)
|
||||
|
||||
let hue = 0
|
||||
|
||||
switch (max) {
|
||||
case normalizedRed:
|
||||
hue = (normalizedGreen - normalizedBlue) / delta + (normalizedGreen < normalizedBlue ? 6 : 0)
|
||||
break
|
||||
case normalizedGreen:
|
||||
hue = (normalizedBlue - normalizedRed) / delta + 2
|
||||
break
|
||||
default:
|
||||
hue = (normalizedRed - normalizedGreen) / delta + 4
|
||||
break
|
||||
}
|
||||
|
||||
return [hue * 60, saturation, lightness]
|
||||
}
|
||||
|
||||
function hslToHex(hue: number, saturation: number, lightness: number) {
|
||||
if (saturation === 0) {
|
||||
const value = Math.round(lightness * 255)
|
||||
return rgbToHex(value, value, value)
|
||||
}
|
||||
|
||||
const hueToRgb = (p: number, q: number, t: number) => {
|
||||
let normalizedT = t
|
||||
|
||||
if (normalizedT < 0) normalizedT += 1
|
||||
if (normalizedT > 1) normalizedT -= 1
|
||||
if (normalizedT < 1 / 6) return p + (q - p) * 6 * normalizedT
|
||||
if (normalizedT < 1 / 2) return q
|
||||
if (normalizedT < 2 / 3) return p + (q - p) * (2 / 3 - normalizedT) * 6
|
||||
return p
|
||||
}
|
||||
|
||||
const normalizedHue = hue / 360
|
||||
const q = lightness < 0.5
|
||||
? lightness * (1 + saturation)
|
||||
: lightness + saturation - lightness * saturation
|
||||
const p = 2 * lightness - q
|
||||
const red = hueToRgb(p, q, normalizedHue + 1 / 3)
|
||||
const green = hueToRgb(p, q, normalizedHue)
|
||||
const blue = hueToRgb(p, q, normalizedHue - 1 / 3)
|
||||
|
||||
return rgbToHex(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255))
|
||||
}
|
||||
|
||||
function rgbToHex(red: number, green: number, blue: number) {
|
||||
return `#${[red, green, blue]
|
||||
.map((value) => value.toString(16).padStart(2, "0"))
|
||||
.join("")}`
|
||||
}
|
||||
17
packages/ui/src/react/index.ts
Normal file
17
packages/ui/src/react/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export {
|
||||
getSeededPaperGrainGradientConfig,
|
||||
getSeededPaperMeshGradientConfig,
|
||||
paperGrainGradientDefaults,
|
||||
paperMeshGradientDefaults,
|
||||
resolvePaperGrainGradientConfig,
|
||||
resolvePaperMeshGradientConfig,
|
||||
} from "../common/paper"
|
||||
export type {
|
||||
PaperGrainGradientConfig,
|
||||
PaperMeshGradientConfig,
|
||||
SeededPaperOption,
|
||||
} from "../common/paper"
|
||||
export { PaperGrainGradient } from "./paper/grain-gradient"
|
||||
export type { PaperGrainGradientProps } from "./paper/grain-gradient"
|
||||
export { PaperMeshGradient } from "./paper/mesh-gradient"
|
||||
export type { PaperMeshGradientProps } from "./paper/mesh-gradient"
|
||||
101
packages/ui/src/react/paper/grain-gradient.tsx
Normal file
101
packages/ui/src/react/paper/grain-gradient.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
defaultObjectSizing,
|
||||
defaultPatternSizing,
|
||||
type GrainGradientShape,
|
||||
} from "@paper-design/shaders"
|
||||
import { GrainGradient, type GrainGradientProps } from "@paper-design/shaders-react"
|
||||
import { resolvePaperGrainGradientConfig } from "../../common/paper"
|
||||
|
||||
export interface PaperGrainGradientProps
|
||||
extends Omit<
|
||||
GrainGradientProps,
|
||||
"colorBack" | "colors" | "softness" | "intensity" | "noise" | "shape" | "speed" | "frame"
|
||||
> {
|
||||
seed?: string
|
||||
fill?: boolean
|
||||
colorBack?: string
|
||||
colors?: string[]
|
||||
softness?: number
|
||||
intensity?: number
|
||||
noise?: number
|
||||
shape?: GrainGradientProps["shape"]
|
||||
speed?: number
|
||||
frame?: number
|
||||
}
|
||||
|
||||
export function PaperGrainGradient({
|
||||
seed,
|
||||
fill = true,
|
||||
colorBack,
|
||||
colors,
|
||||
softness,
|
||||
intensity,
|
||||
noise,
|
||||
shape,
|
||||
speed,
|
||||
frame,
|
||||
fit,
|
||||
rotation,
|
||||
scale,
|
||||
originX,
|
||||
originY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
worldWidth,
|
||||
worldHeight,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: PaperGrainGradientProps) {
|
||||
const resolved = resolvePaperGrainGradientConfig({
|
||||
seed,
|
||||
colorBack,
|
||||
colors,
|
||||
softness,
|
||||
intensity,
|
||||
noise,
|
||||
shape,
|
||||
speed,
|
||||
frame,
|
||||
})
|
||||
|
||||
const sizingDefaults = getSizingDefaults(resolved.shape)
|
||||
|
||||
return (
|
||||
<GrainGradient
|
||||
{...props}
|
||||
width={width ?? (fill ? "100%" : undefined)}
|
||||
height={height ?? (fill ? "100%" : undefined)}
|
||||
fit={fit ?? sizingDefaults.fit}
|
||||
rotation={rotation ?? sizingDefaults.rotation}
|
||||
scale={scale ?? sizingDefaults.scale}
|
||||
originX={originX ?? sizingDefaults.originX}
|
||||
originY={originY ?? sizingDefaults.originY}
|
||||
offsetX={offsetX ?? sizingDefaults.offsetX}
|
||||
offsetY={offsetY ?? sizingDefaults.offsetY}
|
||||
worldWidth={worldWidth ?? sizingDefaults.worldWidth}
|
||||
worldHeight={worldHeight ?? sizingDefaults.worldHeight}
|
||||
colorBack={resolved.colorBack}
|
||||
colors={resolved.colors}
|
||||
softness={resolved.softness}
|
||||
intensity={resolved.intensity}
|
||||
noise={resolved.noise}
|
||||
shape={resolved.shape}
|
||||
speed={resolved.speed}
|
||||
frame={resolved.frame}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function getSizingDefaults(shape: GrainGradientShape) {
|
||||
switch (shape) {
|
||||
case "wave":
|
||||
case "dots":
|
||||
case "truchet":
|
||||
return defaultPatternSizing
|
||||
default:
|
||||
return defaultObjectSizing
|
||||
}
|
||||
}
|
||||
61
packages/ui/src/react/paper/mesh-gradient.tsx
Normal file
61
packages/ui/src/react/paper/mesh-gradient.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client"
|
||||
|
||||
import { MeshGradient, type MeshGradientProps } from "@paper-design/shaders-react"
|
||||
import { resolvePaperMeshGradientConfig } from "../../common/paper"
|
||||
|
||||
export interface PaperMeshGradientProps
|
||||
extends Omit<
|
||||
MeshGradientProps,
|
||||
"colors" | "distortion" | "swirl" | "grainMixer" | "grainOverlay" | "speed" | "frame"
|
||||
> {
|
||||
seed?: string
|
||||
fill?: boolean
|
||||
colors?: string[]
|
||||
distortion?: number
|
||||
swirl?: number
|
||||
grainMixer?: number
|
||||
grainOverlay?: number
|
||||
speed?: number
|
||||
frame?: number
|
||||
}
|
||||
|
||||
export function PaperMeshGradient({
|
||||
seed,
|
||||
fill = true,
|
||||
colors,
|
||||
distortion,
|
||||
swirl,
|
||||
grainMixer,
|
||||
grainOverlay,
|
||||
speed,
|
||||
frame,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: PaperMeshGradientProps) {
|
||||
const resolved = resolvePaperMeshGradientConfig({
|
||||
seed,
|
||||
colors,
|
||||
distortion,
|
||||
swirl,
|
||||
grainMixer,
|
||||
grainOverlay,
|
||||
speed,
|
||||
frame,
|
||||
})
|
||||
|
||||
return (
|
||||
<MeshGradient
|
||||
{...props}
|
||||
width={width ?? (fill ? "100%" : undefined)}
|
||||
height={height ?? (fill ? "100%" : undefined)}
|
||||
colors={resolved.colors}
|
||||
distortion={resolved.distortion}
|
||||
swirl={resolved.swirl}
|
||||
grainMixer={resolved.grainMixer}
|
||||
grainOverlay={resolved.grainOverlay}
|
||||
speed={resolved.speed}
|
||||
frame={resolved.frame}
|
||||
/>
|
||||
)
|
||||
}
|
||||
17
packages/ui/src/solid/index.ts
Normal file
17
packages/ui/src/solid/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export {
|
||||
getSeededPaperGrainGradientConfig,
|
||||
getSeededPaperMeshGradientConfig,
|
||||
paperGrainGradientDefaults,
|
||||
paperMeshGradientDefaults,
|
||||
resolvePaperGrainGradientConfig,
|
||||
resolvePaperMeshGradientConfig,
|
||||
} from "../common/paper"
|
||||
export type {
|
||||
PaperGrainGradientConfig,
|
||||
PaperMeshGradientConfig,
|
||||
SeededPaperOption,
|
||||
} from "../common/paper"
|
||||
export { PaperGrainGradient } from "./paper/grain-gradient"
|
||||
export type { PaperGrainGradientProps } from "./paper/grain-gradient"
|
||||
export { PaperMeshGradient } from "./paper/mesh-gradient"
|
||||
export type { PaperMeshGradientProps } from "./paper/mesh-gradient"
|
||||
125
packages/ui/src/solid/paper/grain-gradient.tsx
Normal file
125
packages/ui/src/solid/paper/grain-gradient.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
defaultObjectSizing,
|
||||
defaultPatternSizing,
|
||||
getShaderColorFromString,
|
||||
getShaderNoiseTexture,
|
||||
grainGradientFragmentShader,
|
||||
GrainGradientShapes,
|
||||
ShaderFitOptions,
|
||||
type GrainGradientParams,
|
||||
} from "@paper-design/shaders"
|
||||
import type { JSX } from "solid-js"
|
||||
import { resolvePaperGrainGradientConfig } from "../../common/paper"
|
||||
import { SolidShaderMount } from "./shader-mount"
|
||||
|
||||
type SharedGrainProps = Pick<
|
||||
GrainGradientParams,
|
||||
"fit" | "rotation" | "scale" | "originX" | "originY" | "offsetX" | "offsetY" | "worldWidth" | "worldHeight"
|
||||
>
|
||||
|
||||
export interface PaperGrainGradientProps
|
||||
extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "ref">,
|
||||
Partial<SharedGrainProps> {
|
||||
ref?: (element: HTMLDivElement) => void
|
||||
seed?: string
|
||||
fill?: boolean
|
||||
colorBack?: string
|
||||
colors?: string[]
|
||||
softness?: number
|
||||
intensity?: number
|
||||
noise?: number
|
||||
shape?: GrainGradientParams["shape"]
|
||||
speed?: number
|
||||
frame?: number
|
||||
minPixelRatio?: number
|
||||
maxPixelCount?: number
|
||||
webGlContextAttributes?: WebGLContextAttributes
|
||||
width?: string | number
|
||||
height?: string | number
|
||||
}
|
||||
|
||||
export function PaperGrainGradient({
|
||||
seed,
|
||||
fill = true,
|
||||
colorBack,
|
||||
colors,
|
||||
softness,
|
||||
intensity,
|
||||
noise,
|
||||
shape,
|
||||
speed,
|
||||
frame,
|
||||
fit,
|
||||
rotation,
|
||||
scale,
|
||||
originX,
|
||||
originY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
worldWidth,
|
||||
worldHeight,
|
||||
minPixelRatio,
|
||||
maxPixelCount,
|
||||
webGlContextAttributes,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: PaperGrainGradientProps) {
|
||||
const resolved = resolvePaperGrainGradientConfig({
|
||||
seed,
|
||||
colorBack,
|
||||
colors,
|
||||
softness,
|
||||
intensity,
|
||||
noise,
|
||||
shape,
|
||||
speed,
|
||||
frame,
|
||||
})
|
||||
|
||||
const sizingDefaults = getSizingDefaults(resolved.shape)
|
||||
|
||||
return (
|
||||
<SolidShaderMount
|
||||
{...props}
|
||||
width={width ?? (fill ? "100%" : undefined)}
|
||||
height={height ?? (fill ? "100%" : undefined)}
|
||||
speed={resolved.speed}
|
||||
frame={resolved.frame}
|
||||
minPixelRatio={minPixelRatio}
|
||||
maxPixelCount={maxPixelCount}
|
||||
webGlContextAttributes={webGlContextAttributes}
|
||||
fragmentShader={grainGradientFragmentShader}
|
||||
uniforms={{
|
||||
u_colorBack: getShaderColorFromString(resolved.colorBack),
|
||||
u_colors: resolved.colors.map(getShaderColorFromString),
|
||||
u_colorsCount: resolved.colors.length,
|
||||
u_softness: resolved.softness,
|
||||
u_intensity: resolved.intensity,
|
||||
u_noise: resolved.noise,
|
||||
u_shape: GrainGradientShapes[resolved.shape],
|
||||
u_noiseTexture: getShaderNoiseTexture(),
|
||||
u_fit: ShaderFitOptions[fit ?? sizingDefaults.fit],
|
||||
u_scale: scale ?? sizingDefaults.scale,
|
||||
u_rotation: rotation ?? sizingDefaults.rotation,
|
||||
u_offsetX: offsetX ?? sizingDefaults.offsetX,
|
||||
u_offsetY: offsetY ?? sizingDefaults.offsetY,
|
||||
u_originX: originX ?? sizingDefaults.originX,
|
||||
u_originY: originY ?? sizingDefaults.originY,
|
||||
u_worldWidth: worldWidth ?? sizingDefaults.worldWidth,
|
||||
u_worldHeight: worldHeight ?? sizingDefaults.worldHeight,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function getSizingDefaults(shape: NonNullable<GrainGradientParams["shape"]>) {
|
||||
switch (shape) {
|
||||
case "wave":
|
||||
case "dots":
|
||||
case "truchet":
|
||||
return defaultPatternSizing
|
||||
default:
|
||||
return defaultObjectSizing
|
||||
}
|
||||
}
|
||||
104
packages/ui/src/solid/paper/mesh-gradient.tsx
Normal file
104
packages/ui/src/solid/paper/mesh-gradient.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
defaultObjectSizing,
|
||||
getShaderColorFromString,
|
||||
meshGradientFragmentShader,
|
||||
ShaderFitOptions,
|
||||
type MeshGradientParams,
|
||||
} from "@paper-design/shaders"
|
||||
import type { JSX } from "solid-js"
|
||||
import { resolvePaperMeshGradientConfig } from "../../common/paper"
|
||||
import { SolidShaderMount } from "./shader-mount"
|
||||
|
||||
type SharedMeshProps = Pick<
|
||||
MeshGradientParams,
|
||||
"fit" | "rotation" | "scale" | "originX" | "originY" | "offsetX" | "offsetY" | "worldWidth" | "worldHeight"
|
||||
>
|
||||
|
||||
export interface PaperMeshGradientProps
|
||||
extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "ref">,
|
||||
Partial<SharedMeshProps> {
|
||||
ref?: (element: HTMLDivElement) => void
|
||||
seed?: string
|
||||
fill?: boolean
|
||||
colors?: string[]
|
||||
distortion?: number
|
||||
swirl?: number
|
||||
grainMixer?: number
|
||||
grainOverlay?: number
|
||||
speed?: number
|
||||
frame?: number
|
||||
minPixelRatio?: number
|
||||
maxPixelCount?: number
|
||||
webGlContextAttributes?: WebGLContextAttributes
|
||||
width?: string | number
|
||||
height?: string | number
|
||||
}
|
||||
|
||||
export function PaperMeshGradient({
|
||||
seed,
|
||||
fill = true,
|
||||
colors,
|
||||
distortion,
|
||||
swirl,
|
||||
grainMixer,
|
||||
grainOverlay,
|
||||
speed,
|
||||
frame,
|
||||
fit = defaultObjectSizing.fit,
|
||||
rotation = defaultObjectSizing.rotation,
|
||||
scale = defaultObjectSizing.scale,
|
||||
originX = defaultObjectSizing.originX,
|
||||
originY = defaultObjectSizing.originY,
|
||||
offsetX = defaultObjectSizing.offsetX,
|
||||
offsetY = defaultObjectSizing.offsetY,
|
||||
worldWidth = defaultObjectSizing.worldWidth,
|
||||
worldHeight = defaultObjectSizing.worldHeight,
|
||||
minPixelRatio,
|
||||
maxPixelCount,
|
||||
webGlContextAttributes,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: PaperMeshGradientProps) {
|
||||
const resolved = resolvePaperMeshGradientConfig({
|
||||
seed,
|
||||
colors,
|
||||
distortion,
|
||||
swirl,
|
||||
grainMixer,
|
||||
grainOverlay,
|
||||
speed,
|
||||
frame,
|
||||
})
|
||||
|
||||
return (
|
||||
<SolidShaderMount
|
||||
{...props}
|
||||
width={width ?? (fill ? "100%" : undefined)}
|
||||
height={height ?? (fill ? "100%" : undefined)}
|
||||
speed={resolved.speed}
|
||||
frame={resolved.frame}
|
||||
minPixelRatio={minPixelRatio}
|
||||
maxPixelCount={maxPixelCount}
|
||||
webGlContextAttributes={webGlContextAttributes}
|
||||
fragmentShader={meshGradientFragmentShader}
|
||||
uniforms={{
|
||||
u_colors: resolved.colors.map(getShaderColorFromString),
|
||||
u_colorsCount: resolved.colors.length,
|
||||
u_distortion: resolved.distortion,
|
||||
u_swirl: resolved.swirl,
|
||||
u_grainMixer: resolved.grainMixer,
|
||||
u_grainOverlay: resolved.grainOverlay,
|
||||
u_fit: ShaderFitOptions[fit],
|
||||
u_rotation: rotation,
|
||||
u_scale: scale,
|
||||
u_offsetX: offsetX,
|
||||
u_offsetY: offsetY,
|
||||
u_originX: originX,
|
||||
u_originY: originY,
|
||||
u_worldWidth: worldWidth,
|
||||
u_worldHeight: worldHeight,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
115
packages/ui/src/solid/paper/shader-mount.tsx
Normal file
115
packages/ui/src/solid/paper/shader-mount.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { ShaderMount, type ShaderMountUniforms } from "@paper-design/shaders"
|
||||
import { createEffect, onCleanup, onMount, splitProps, type JSX } from "solid-js"
|
||||
|
||||
type SolidShaderMountProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, "ref"> & {
|
||||
ref?: (element: HTMLDivElement) => void
|
||||
fragmentShader: string
|
||||
uniforms: ShaderMountUniforms
|
||||
speed?: number
|
||||
frame?: number
|
||||
minPixelRatio?: number
|
||||
maxPixelCount?: number
|
||||
webGlContextAttributes?: WebGLContextAttributes
|
||||
width?: string | number
|
||||
height?: string | number
|
||||
}
|
||||
|
||||
export function SolidShaderMount(props: SolidShaderMountProps) {
|
||||
const [local, rest] = splitProps(props, [
|
||||
"ref",
|
||||
"fragmentShader",
|
||||
"uniforms",
|
||||
"speed",
|
||||
"frame",
|
||||
"minPixelRatio",
|
||||
"maxPixelCount",
|
||||
"webGlContextAttributes",
|
||||
"width",
|
||||
"height",
|
||||
"style",
|
||||
])
|
||||
|
||||
let element: HTMLDivElement | undefined
|
||||
let shaderMount: ShaderMount | undefined
|
||||
|
||||
onMount(() => {
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
|
||||
shaderMount = new ShaderMount(
|
||||
element,
|
||||
local.fragmentShader,
|
||||
local.uniforms,
|
||||
local.webGlContextAttributes,
|
||||
local.speed,
|
||||
local.frame,
|
||||
local.minPixelRatio,
|
||||
local.maxPixelCount,
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
shaderMount?.dispose()
|
||||
shaderMount = undefined
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
shaderMount?.setUniforms(local.uniforms)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
shaderMount?.setSpeed(local.speed)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (local.frame !== undefined) {
|
||||
shaderMount?.setFrame(local.frame)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
shaderMount?.setMinPixelRatio(local.minPixelRatio)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
shaderMount?.setMaxPixelCount(local.maxPixelCount)
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
ref={(node) => {
|
||||
element = node
|
||||
local.ref?.(node)
|
||||
}}
|
||||
style={mergeStyle(local.style, local.width, local.height)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function mergeStyle(
|
||||
style: JSX.CSSProperties | string | undefined,
|
||||
width: string | number | undefined,
|
||||
height: string | number | undefined,
|
||||
) {
|
||||
if (typeof style === "string") {
|
||||
return [
|
||||
width !== undefined ? `width:${toCssSize(width)}` : "",
|
||||
height !== undefined ? `height:${toCssSize(height)}` : "",
|
||||
style,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(";")
|
||||
}
|
||||
|
||||
return {
|
||||
...(style ?? {}),
|
||||
...(width !== undefined ? { width: toCssSize(width) } : {}),
|
||||
...(height !== undefined ? { height: toCssSize(height) } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function toCssSize(value: string | number) {
|
||||
return typeof value === "number" ? `${value}px` : value
|
||||
}
|
||||
19
packages/ui/tsconfig.react.json
Normal file
19
packages/ui/tsconfig.react.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src/common/**/*",
|
||||
"src/react/**/*"
|
||||
]
|
||||
}
|
||||
20
packages/ui/tsconfig.solid.json
Normal file
20
packages/ui/tsconfig.solid.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js/h"
|
||||
},
|
||||
"include": [
|
||||
"src/common/**/*",
|
||||
"src/solid/**/*"
|
||||
]
|
||||
}
|
||||
46
packages/ui/tsup.config.ts
Normal file
46
packages/ui/tsup.config.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineConfig } from "tsup"
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: {
|
||||
"react/index": "src/react/index.ts",
|
||||
},
|
||||
tsconfig: "./tsconfig.react.json",
|
||||
format: ["esm"],
|
||||
dts: {
|
||||
tsconfig: "./tsconfig.react.json",
|
||||
},
|
||||
clean: true,
|
||||
target: "es2022",
|
||||
platform: "browser",
|
||||
sourcemap: false,
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
external: ["react", "react/jsx-runtime"],
|
||||
esbuildOptions(options) {
|
||||
options.jsx = "automatic"
|
||||
options.jsxImportSource = "react"
|
||||
},
|
||||
},
|
||||
{
|
||||
entry: {
|
||||
"solid/index": "src/solid/index.ts",
|
||||
},
|
||||
tsconfig: "./tsconfig.solid.json",
|
||||
format: ["esm"],
|
||||
dts: {
|
||||
tsconfig: "./tsconfig.solid.json",
|
||||
},
|
||||
clean: false,
|
||||
target: "es2022",
|
||||
platform: "browser",
|
||||
sourcemap: false,
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
external: ["solid-js", "solid-js/jsx-runtime"],
|
||||
esbuildOptions(options) {
|
||||
options.jsx = "automatic"
|
||||
options.jsxImportSource = "solid-js/h"
|
||||
},
|
||||
},
|
||||
])
|
||||
872
pnpm-lock.yaml
generated
872
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user