From 1047a1f5fe2256eeace252614bd6b2c56856890f Mon Sep 17 00:00:00 2001 From: John Lightner Date: Tue, 24 Mar 2026 22:00:10 -0500 Subject: [PATCH] Versioning, drafts, resizable editor, My Shaders, 200 seed shaders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architecture — Shader versioning & draft system: - New shader_versions table: immutable snapshots of every edit - Shaders now have status: draft, published, archived - current_version counter tracks version number - Every create/update creates a ShaderVersion record - Restore-from-version endpoint creates new version (never destructive) - Drafts are private, only visible to author - Forks start as drafts - Free tier rate limit applies only to published shaders (drafts unlimited) Architecture — Platform identity: - System account 'fractafrag' (UUID 00000000-...-000001) created in init.sql - is_system flag on users and shaders - system_label field: 'fractafrag-curated', future: 'fractafrag-generated' - Feed/explore can filter by is_system - System shaders display distinctly from user/AI content API changes: - GET /shaders/mine — user workspace (drafts, published, archived) - GET /shaders/{id}/versions — version history - GET /shaders/{id}/versions/{n} — specific version - POST /shaders/{id}/versions/{n}/restore — restore old version - POST /shaders accepts status: 'draft' | 'published' - PUT /shaders/{id} accepts change_note for version descriptions - PUT status transitions: draft→published, published→archived, archived→published Frontend — Editor improvements: - Resizable split pane with drag handle (20-80% range, smooth col-resize cursor) - Save Draft button (creates/updates as draft, no publish) - Publish button (validates, publishes, redirects to shader page) - Version badge shows current version number when editing existing - Owner detection: editing own shader vs forking someone else's - Saved status indicator ('Draft saved', 'Published') Frontend — My Shaders workspace: - /my-shaders route with status tabs (All, Draft, Published, Archived) - Count badges per tab - Status badges on shader cards (draft=yellow, published=green, archived=grey) - Version badges (v1, v2, etc.) - Quick actions: Edit, Publish, Archive, Restore, Delete per status - Drafts link to editor, published link to detail page Seed data — 200 fractafrag-curated shaders: - 171 2D + 29 3D shaders - 500 unique tags across all shaders - All 200 titles are unique - Covers: fractals (Mandelbrot, Julia sets), noise (fbm, Voronoi, Perlin), raymarching (metaballs, terrain, torus knots, metall/glass), effects (glitch, VHS, plasma, aurora, lightning, fireworks), patterns (circuit, hex grid, stained glass, herringbone, moiré), physics (wave interference, pendulum, caustics, gravity lens), minimal (single shapes, gradients, dot grids), nature (ink, watercolor, smoke, sand garden, coral, nebula), color theory (RGB separation, CMY overlap, hue wheel), domain warping (acid trip, lava rift, storm eye), particles (fireflies, snow, ember, bubbles) - Each shader has style_metadata (chaos_level, color_temperature, motion_type) - Distributed creation times over 30 days for feed ranking variety - Random initial scores for algorithm testing - All authored by 'fractafrag' system account, is_system=true - system_label='fractafrag-curated' for clear provenance Schema: - shader_versions table with (shader_id, version_number) unique constraint - HNSW indexes on version lookup - System account indexes - Status-aware feed indexes --- db/init.sql | 167 +- scripts/seed_shaders.py | 2043 +++++++++++++++++++ services/api/app/models/__init__.py | 6 +- services/api/app/models/models.py | 35 +- services/api/app/routers/feed.py | 17 +- services/api/app/routers/shaders.py | 286 ++- services/api/app/schemas/schemas.py | 32 +- services/frontend/src/App.tsx | 2 + services/frontend/src/components/Navbar.tsx | 1 + services/frontend/src/pages/Editor.tsx | 144 +- services/frontend/src/pages/MyShaders.tsx | 197 ++ 11 files changed, 2765 insertions(+), 165 deletions(-) create mode 100644 scripts/seed_shaders.py create mode 100644 services/frontend/src/pages/MyShaders.tsx diff --git a/db/init.sql b/db/init.sql index 798a6e1..bfd5109 100644 --- a/db/init.sql +++ b/db/init.sql @@ -16,6 +16,7 @@ CREATE TABLE users ( password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', -- user, moderator, admin trust_tier TEXT NOT NULL DEFAULT 'standard', -- standard, creator, trusted_api + is_system BOOLEAN NOT NULL DEFAULT FALSE, -- platform system account (fractafrag) stripe_customer_id TEXT, subscription_tier TEXT DEFAULT 'free', -- free, pro, studio ai_credits_remaining INTEGER DEFAULT 0, @@ -38,21 +39,25 @@ CREATE TABLE shaders ( title TEXT NOT NULL, description TEXT, glsl_code TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'published', -- draft, published, archived is_public BOOLEAN DEFAULT TRUE, is_ai_generated BOOLEAN DEFAULT FALSE, - ai_provider TEXT, -- anthropic, openai, ollama, null + is_system BOOLEAN DEFAULT FALSE, -- generated by fractafrag platform + ai_provider TEXT, -- anthropic, openai, ollama, null + system_label TEXT, -- e.g. 'fractafrag-curated', 'fractafrag-generated' thumbnail_url TEXT, preview_url TEXT, - render_status TEXT DEFAULT 'pending', -- pending, rendering, ready, failed - style_vector vector(512), -- pgvector: visual style embedding - style_metadata JSONB, -- { chaos_level, color_temp, motion_type, ... } + render_status TEXT DEFAULT 'pending', -- pending, rendering, ready, failed + style_vector vector(512), -- pgvector: visual style embedding + style_metadata JSONB, -- { chaos_level, color_temp, motion_type, ... } tags TEXT[], - shader_type TEXT DEFAULT '2d', -- 2d, 3d, audio-reactive + shader_type TEXT DEFAULT '2d', -- 2d, 3d, audio-reactive forked_from UUID REFERENCES shaders(id) ON DELETE SET NULL, + current_version INTEGER NOT NULL DEFAULT 1, -- current version number view_count INTEGER DEFAULT 0, - score FLOAT DEFAULT 0, -- cached hot score for feed ranking + score FLOAT DEFAULT 0, -- cached hot score for feed ranking -- Creator economy stubs (Section 11f) - access_tier TEXT DEFAULT 'open', -- open, source_locked, commercial + access_tier TEXT DEFAULT 'open', source_unlock_price_cents INTEGER, commercial_license_price_cents INTEGER, verified_creator_shader BOOLEAN DEFAULT FALSE, @@ -61,15 +66,33 @@ CREATE TABLE shaders ( updated_at TIMESTAMPTZ DEFAULT NOW() ); +-- ════════════════════════════════════════════════════════════ +-- SHADER VERSIONS — immutable snapshots of each edit +-- ════════════════════════════════════════════════════════════ +CREATE TABLE shader_versions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + shader_id UUID NOT NULL REFERENCES shaders(id) ON DELETE CASCADE, + version_number INTEGER NOT NULL, + glsl_code TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT, + tags TEXT[], + style_metadata JSONB, + change_note TEXT, -- optional: "fixed the color bleeding", "added mouse interaction" + thumbnail_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE (shader_id, version_number) +); + -- ════════════════════════════════════════════════════════════ -- VOTES -- ════════════════════════════════════════════════════════════ CREATE TABLE votes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID REFERENCES users(id) ON DELETE CASCADE, - shader_id UUID REFERENCES shaders(id) ON DELETE CASCADE, - value SMALLINT NOT NULL CHECK (value IN (-1, 1)), - created_at TIMESTAMPTZ DEFAULT NOW(), + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + shader_id UUID REFERENCES shaders(id) ON DELETE CASCADE, + value SMALLINT NOT NULL CHECK (value IN (-1, 1)), + created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE (user_id, shader_id) ); @@ -78,10 +101,10 @@ CREATE TABLE votes ( -- ════════════════════════════════════════════════════════════ CREATE TABLE engagement_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID REFERENCES users(id) ON DELETE SET NULL, -- null for anonymous - session_id TEXT, -- anonymous session token + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + session_id TEXT, shader_id UUID REFERENCES shaders(id) ON DELETE CASCADE, - event_type TEXT NOT NULL, -- dwell, replay, share, generate_similar + event_type TEXT NOT NULL, dwell_secs FLOAT, metadata JSONB, created_at TIMESTAMPTZ DEFAULT NOW() @@ -91,21 +114,20 @@ CREATE TABLE engagement_events ( -- DESIRES / BOUNTIES -- ════════════════════════════════════════════════════════════ CREATE TABLE desires ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - author_id UUID REFERENCES users(id) ON DELETE SET NULL, - prompt_text TEXT NOT NULL, - prompt_embedding vector(512), -- embedded for similarity grouping - style_hints JSONB, -- { chaos_level, color_temp, etc } - tip_amount_cents INTEGER DEFAULT 0, - status TEXT DEFAULT 'open', -- open, in_progress, fulfilled, expired - heat_score FLOAT DEFAULT 1, -- updated as similar desires accumulate - fulfilled_by_shader UUID REFERENCES shaders(id) ON DELETE SET NULL, - fulfilled_at TIMESTAMPTZ, - expires_at TIMESTAMPTZ, - created_at TIMESTAMPTZ DEFAULT NOW() + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + author_id UUID REFERENCES users(id) ON DELETE SET NULL, + prompt_text TEXT NOT NULL, + prompt_embedding vector(512), + style_hints JSONB, + tip_amount_cents INTEGER DEFAULT 0, + status TEXT DEFAULT 'open', + heat_score FLOAT DEFAULT 1, + fulfilled_by_shader UUID REFERENCES shaders(id) ON DELETE SET NULL, + fulfilled_at TIMESTAMPTZ, + expires_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW() ); --- Similar desire grouping (many-to-many) CREATE TABLE desire_clusters ( cluster_id UUID, desire_id UUID REFERENCES desires(id) ON DELETE CASCADE, @@ -117,27 +139,27 @@ CREATE TABLE desire_clusters ( -- BOUNTY TIPS -- ════════════════════════════════════════════════════════════ CREATE TABLE bounty_tips ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - desire_id UUID REFERENCES desires(id) ON DELETE CASCADE, - tipper_id UUID REFERENCES users(id) ON DELETE SET NULL, - amount_cents INTEGER NOT NULL, - stripe_payment_intent_id TEXT, - status TEXT DEFAULT 'held', -- held, released, refunded - created_at TIMESTAMPTZ DEFAULT NOW() + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + desire_id UUID REFERENCES desires(id) ON DELETE CASCADE, + tipper_id UUID REFERENCES users(id) ON DELETE SET NULL, + amount_cents INTEGER NOT NULL, + stripe_payment_intent_id TEXT, + status TEXT DEFAULT 'held', + created_at TIMESTAMPTZ DEFAULT NOW() ); -- ════════════════════════════════════════════════════════════ -- CREATOR PAYOUTS -- ════════════════════════════════════════════════════════════ CREATE TABLE creator_payouts ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - creator_id UUID REFERENCES users(id) ON DELETE SET NULL, - desire_id UUID REFERENCES desires(id) ON DELETE SET NULL, + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + creator_id UUID REFERENCES users(id) ON DELETE SET NULL, + desire_id UUID REFERENCES desires(id) ON DELETE SET NULL, gross_amount_cents INTEGER, - platform_fee_cents INTEGER, -- 10% - net_amount_cents INTEGER, -- 90% + platform_fee_cents INTEGER, + net_amount_cents INTEGER, stripe_transfer_id TEXT, - status TEXT DEFAULT 'pending', -- pending, processing, completed, failed + status TEXT DEFAULT 'pending', created_at TIMESTAMPTZ DEFAULT NOW() ); @@ -145,17 +167,17 @@ CREATE TABLE creator_payouts ( -- API KEYS (for MCP clients) -- ════════════════════════════════════════════════════════════ CREATE TABLE api_keys ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID REFERENCES users(id) ON DELETE CASCADE, - key_hash TEXT UNIQUE NOT NULL, -- bcrypt hash of the actual key - key_prefix TEXT NOT NULL, -- first 8 chars for display (ff_key_XXXXXXXX) - name TEXT, -- user-given label - trust_tier TEXT DEFAULT 'probation', -- probation, trusted, premium - submissions_approved INTEGER DEFAULT 0, - rate_limit_per_hour INTEGER DEFAULT 10, - last_used_at TIMESTAMPTZ, - created_at TIMESTAMPTZ DEFAULT NOW(), - revoked_at TIMESTAMPTZ + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + key_hash TEXT UNIQUE NOT NULL, + key_prefix TEXT NOT NULL, + name TEXT, + trust_tier TEXT DEFAULT 'probation', + submissions_approved INTEGER DEFAULT 0, + rate_limit_per_hour INTEGER DEFAULT 10, + last_used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + revoked_at TIMESTAMPTZ ); -- ════════════════════════════════════════════════════════════ @@ -168,7 +190,7 @@ CREATE TABLE generation_log ( provider TEXT NOT NULL, prompt_text TEXT, tokens_used INTEGER, - cost_cents INTEGER, -- platform cost for credit-based generations + cost_cents INTEGER, success BOOLEAN, created_at TIMESTAMPTZ DEFAULT NOW() ); @@ -192,7 +214,7 @@ CREATE TABLE source_unlocks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), shader_id UUID REFERENCES shaders(id) ON DELETE CASCADE, buyer_id UUID REFERENCES users(id) ON DELETE SET NULL, - license_type TEXT NOT NULL, -- personal, commercial + license_type TEXT NOT NULL, amount_cents INTEGER NOT NULL, platform_fee_cents INTEGER NOT NULL, stripe_payment_intent_id TEXT, @@ -215,22 +237,18 @@ CREATE TABLE creator_engagement_snapshots ( -- ════════════════════════════════════════════════════════════ -- Feed performance -CREATE INDEX idx_shaders_score ON shaders(score DESC) WHERE is_public = TRUE; -CREATE INDEX idx_shaders_created ON shaders(created_at DESC) WHERE is_public = TRUE; +CREATE INDEX idx_shaders_score ON shaders(score DESC) WHERE is_public = TRUE AND status = 'published'; +CREATE INDEX idx_shaders_created ON shaders(created_at DESC) WHERE is_public = TRUE AND status = 'published'; CREATE INDEX idx_shaders_tags ON shaders USING GIN(tags); CREATE INDEX idx_shaders_render_status ON shaders(render_status) WHERE render_status != 'ready'; +CREATE INDEX idx_shaders_status ON shaders(status); +CREATE INDEX idx_shaders_author_status ON shaders(author_id, status, updated_at DESC); +CREATE INDEX idx_shaders_system ON shaders(is_system) WHERE is_system = TRUE; --- Recommendation (pgvector ANN — ivfflat, will rebuild after data exists) --- NOTE: ivfflat indexes require data in the table to build properly. --- Run these AFTER seeding initial data: --- CREATE INDEX idx_shaders_style_vector ON shaders --- USING ivfflat (style_vector vector_cosine_ops) WITH (lists = 100); --- CREATE INDEX idx_users_taste_vector ON users --- USING ivfflat (taste_vector vector_cosine_ops) WITH (lists = 50); --- CREATE INDEX idx_desires_embedding ON desires --- USING ivfflat (prompt_embedding vector_cosine_ops) WITH (lists = 50); +-- Versioning +CREATE INDEX idx_shader_versions_shader ON shader_versions(shader_id, version_number DESC); --- For now, use HNSW (works on empty tables, better perf at small scale) +-- Recommendation (pgvector HNSW — works on empty tables) CREATE INDEX idx_shaders_style_vector ON shaders USING hnsw (style_vector vector_cosine_ops) WITH (m = 16, ef_construction = 64); CREATE INDEX idx_users_taste_vector ON users @@ -263,3 +281,20 @@ CREATE INDEX idx_comments_parent ON comments(parent_id); -- Text search CREATE INDEX idx_shaders_title_trgm ON shaders USING GIN(title gin_trgm_ops); CREATE INDEX idx_desires_prompt_trgm ON desires USING GIN(prompt_text gin_trgm_ops); + +-- ════════════════════════════════════════════════════════════ +-- SYSTEM ACCOUNT: The "fractafrag" platform user +-- All system-generated/curated shaders are authored by this account +-- ════════════════════════════════════════════════════════════ +INSERT INTO users (id, username, email, password_hash, role, trust_tier, is_system, subscription_tier, is_verified_creator) +VALUES ( + '00000000-0000-0000-0000-000000000001', + 'fractafrag', + 'system@fractafrag.local', + '$2b$12$000000000000000000000000000000000000000000000000000000', -- not a valid login + 'admin', + 'trusted_api', + TRUE, + 'studio', + TRUE +); diff --git a/scripts/seed_shaders.py b/scripts/seed_shaders.py new file mode 100644 index 0000000..d4e3d17 --- /dev/null +++ b/scripts/seed_shaders.py @@ -0,0 +1,2043 @@ +""" +Fractafrag Seed Data — 200+ high-quality GLSL shaders. + +Each shader is: +- Written for the 'fractafrag' system account +- Tagged comprehensively for search/filter/algorithm testing +- Flagged as is_system=True with system_label='fractafrag-curated' +- Covers 2D and 3D, every visual style imaginable +- Ready for immediate display in the feed + +Usage: + docker compose exec api python /app/scripts/seed_shaders.py +""" + +import asyncio +import uuid +import random +from datetime import datetime, timedelta, timezone + +# System account UUID +SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000001" + +SHADERS = [] + +def s(title, code, tags, shader_type="2d", description="", style_metadata=None): + """Helper to register a shader.""" + SHADERS.append({ + "title": title, + "code": code.strip(), + "tags": tags, + "shader_type": shader_type, + "description": description, + "style_metadata": style_metadata or {}, + }) + +# ═══════════════════════════════════════════════════════════ +# 2D — MATHEMATICAL / GEOMETRIC +# ═══════════════════════════════════════════════════════════ + +s("Spiral Galaxy", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float a = atan(uv.y, uv.x); + float r = length(uv); + float spiral = sin(a * 5.0 - r * 20.0 + iTime * 2.0); + float glow = 0.02 / (r + 0.01); + vec3 col = vec3(0.3, 0.1, 0.6) * glow + vec3(0.8, 0.4, 0.9) * spiral * exp(-r * 3.0); + fragColor = vec4(col, 1.0); +}""", ["spiral", "galaxy", "mathematical", "glow", "purple"], "2d", "Logarithmic spiral arms with exponential glow falloff", +{"chaos_level": 0.4, "color_temperature": "cool", "motion_type": "rotating"}) + +s("Binary Rain", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float col = 0.0; + for (int i = 0; i < 12; i++) { + float fi = float(i); + vec2 p = fract(uv * vec2(20.0, 1.0) + vec2(fi * 0.37, iTime * (0.5 + fi * 0.1))); + float drop = smoothstep(0.0, 0.1, p.y) * smoothstep(1.0, 0.8, p.y); + col += drop * step(0.7, fract(sin(fi * 73.156) * 43758.5453)); + } + fragColor = vec4(0.0, col * 0.8, col * 0.3, 1.0); +}""", ["matrix", "rain", "digital", "code", "green", "cyberpunk"], "2d", "Digital rain effect inspired by the Matrix", +{"chaos_level": 0.3, "color_temperature": "cool", "motion_type": "falling"}) + +s("Voronoi Shatter", """ +vec2 hash2(vec2 p) { + p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3))); + return fract(sin(p) * 43758.5453); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.y; + vec2 n = floor(uv * 6.0); + vec2 f = fract(uv * 6.0); + float md = 8.0; + vec2 mr; + for (int j = -1; j <= 1; j++) + for (int i = -1; i <= 1; i++) { + vec2 g = vec2(float(i), float(j)); + vec2 o = hash2(n + g); + o = 0.5 + 0.5 * sin(iTime + 6.2831 * o); + vec2 r = g + o - f; + float d = dot(r, r); + if (d < md) { md = d; mr = r; } + } + vec3 col = 0.5 + 0.5 * cos(md * 6.0 + vec3(0, 1, 2) + iTime); + col *= 1.0 - 0.5 * smoothstep(0.0, 0.05, md); + fragColor = vec4(col, 1.0); +}""", ["voronoi", "geometric", "mosaic", "cellular", "colorful", "animated"], "2d", "Animated Voronoi tessellation with iridescent cells", +{"chaos_level": 0.5, "color_temperature": "warm", "motion_type": "morphing"}) + +s("Monochrome Static Interference", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); } +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float t = floor(iTime * 15.0); + float n = hash(uv * 100.0 + t); + float scanline = sin(uv.y * 800.0 + iTime * 50.0) * 0.1; + float bars = step(0.98, fract(uv.y * 30.0 + iTime * 3.0)) * 0.3; + float v = n * 0.7 + scanline + bars; + v = clamp(v, 0.0, 1.0); + fragColor = vec4(vec3(v), 1.0); +}""", ["static", "noise", "monochrome", "glitch", "tv", "analog", "bw"], "2d", "Analog TV static with scanlines and horizontal bars", +{"chaos_level": 0.9, "color_temperature": "monochrome", "motion_type": "chaotic"}) + +s("Mandelbrot Deep Zoom", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + float zoom = pow(2.0, -mod(iTime * 0.5, 30.0)); + vec2 center = vec2(-0.7435669, 0.1314023); + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec2 c = center + uv * zoom; + vec2 z = vec2(0.0); + int iter = 0; + for (int i = 0; i < 256; i++) { + z = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + c; + if (dot(z, z) > 4.0) break; + iter = i; + } + float t = float(iter) / 256.0; + vec3 col = 0.5 + 0.5 * cos(3.0 + t * 15.0 + vec3(0.0, 0.6, 1.0)); + if (iter == 255) col = vec3(0.0); + fragColor = vec4(col, 1.0); +}""", ["mandelbrot", "fractal", "zoom", "mathematical", "infinite", "complex"], "2d", "Continuously zooming into the Mandelbrot set boundary", +{"chaos_level": 0.6, "color_temperature": "cool", "motion_type": "zooming"}) + +s("Neon Grid Pulse", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec2 grid = abs(fract(uv * 8.0 - 0.5) - 0.5); + float d = min(grid.x, grid.y); + float pulse = sin(iTime * 3.0 + length(uv) * 10.0) * 0.5 + 0.5; + float line = smoothstep(0.02, 0.0, d); + vec3 col = vec3(0.0); + col += vec3(0.0, 1.0, 0.8) * line * pulse; + col += vec3(1.0, 0.0, 0.5) * line * (1.0 - pulse); + col += vec3(0.02, 0.0, 0.04); + fragColor = vec4(col, 1.0); +}""", ["grid", "neon", "synthwave", "retro", "tron", "pulse", "cyberpunk"], "2d", "Retro neon grid with alternating color pulses", +{"chaos_level": 0.2, "color_temperature": "cool", "motion_type": "pulsing"}) + +s("Ink in Water", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), + mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); +} +float fbm(vec2 p) { + float v = 0.0, a = 0.5; + mat2 rot = mat2(0.8, 0.6, -0.6, 0.8); + for (int i = 0; i < 7; i++) { v += a * noise(p); p = rot * p * 2.0; a *= 0.5; } + return v; +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float t = iTime * 0.15; + float f1 = fbm(uv * 3.0 + t); + float f2 = fbm(uv * 3.0 + f1 * 2.0 + t * 0.7); + float f3 = fbm(uv * 3.0 + f2 * 2.0); + vec3 col = mix(vec3(0.05, 0.02, 0.1), vec3(0.1, 0.0, 0.3), f1); + col = mix(col, vec3(0.6, 0.1, 0.2), f2 * 0.6); + col = mix(col, vec3(0.9, 0.8, 0.6), f3 * f3 * 0.5); + fragColor = vec4(col, 1.0); +}""", ["fluid", "ink", "water", "organic", "fbm", "noise", "elegant", "dark"], "2d", "Layered fractal brownian motion simulating ink diffusing in water", +{"chaos_level": 0.5, "color_temperature": "warm", "motion_type": "fluid"}) + +s("Kaleidoscope Mirror", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float a = atan(uv.y, uv.x); + float r = length(uv); + float segments = 8.0; + a = mod(a, 6.2831 / segments); + a = abs(a - 3.14159 / segments); + vec2 p = vec2(cos(a), sin(a)) * r; + p += iTime * 0.2; + float pattern = sin(p.x * 15.0) * sin(p.y * 15.0); + pattern += sin(p.x * 7.0 + iTime) * cos(p.y * 9.0 - iTime * 0.5); + vec3 col = 0.5 + 0.5 * cos(pattern * 2.0 + iTime + vec3(0, 2, 4)); + col *= smoothstep(1.5, 0.0, r); + fragColor = vec4(col, 1.0); +}""", ["kaleidoscope", "symmetry", "mirror", "psychedelic", "colorful", "geometric"], "2d", "8-fold kaleidoscope with evolving sine patterns", +{"chaos_level": 0.6, "color_temperature": "neutral", "motion_type": "rotating"}) + +s("Plasma Lava Lamp", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float t = iTime * 0.5; + float v = 0.0; + v += sin(uv.x * 10.0 + t); + v += sin((uv.y * 10.0 + t) * 0.5); + v += sin((uv.x * 10.0 + uv.y * 10.0 + t) * 0.3); + vec2 c = uv * 10.0 + vec2(sin(t * 0.3) * 5.0, cos(t * 0.5) * 5.0); + v += sin(length(c) + t); + v *= 0.5; + vec3 col; + col.r = sin(v * 3.14159) * 0.5 + 0.5; + col.g = sin(v * 3.14159 + 2.094) * 0.3 + 0.2; + col.b = sin(v * 3.14159 + 4.189) * 0.5 + 0.5; + fragColor = vec4(col, 1.0); +}""", ["plasma", "lava", "retro", "colorful", "animated", "smooth", "classic"], "2d", "Classic plasma effect with lava lamp color cycling", +{"chaos_level": 0.3, "color_temperature": "warm", "motion_type": "fluid"}) + +s("Circuit Board", """ +float hash(float n) { return fract(sin(n) * 43758.5453); } +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + vec2 g = floor(uv * 30.0); + vec2 f = fract(uv * 30.0); + float id = hash(g.x + g.y * 137.0); + float trace = 0.0; + if (id > 0.5) trace = step(0.45, f.x) * step(f.x, 0.55); + else trace = step(0.45, f.y) * step(f.y, 0.55); + float node = smoothstep(0.3, 0.25, length(f - 0.5)); + float pulse = sin(iTime * 3.0 + (g.x + g.y) * 0.5) * 0.5 + 0.5; + vec3 col = vec3(0.0, 0.15, 0.1); + col += vec3(0.0, 0.8, 0.4) * trace * (0.3 + 0.7 * pulse); + col += vec3(0.0, 1.0, 0.6) * node * pulse; + fragColor = vec4(col, 1.0); +}""", ["circuit", "digital", "tech", "pcb", "green", "electronic", "grid"], "2d", "Procedural circuit board with animated signal pulses", +{"chaos_level": 0.2, "color_temperature": "cool", "motion_type": "pulsing"}) + +s("Aurora Borealis", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), + mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float t = iTime * 0.2; + float n = 0.0; + n += noise(vec2(uv.x * 3.0 + t, uv.y * 0.5)) * 0.5; + n += noise(vec2(uv.x * 6.0 - t * 0.7, uv.y * 1.0 + t * 0.3)) * 0.25; + n += noise(vec2(uv.x * 12.0 + t * 0.5, uv.y * 2.0)) * 0.125; + float curtain = smoothstep(0.3, 0.7, uv.y) * smoothstep(1.0, 0.6, uv.y); + curtain *= n; + vec3 green = vec3(0.1, 0.9, 0.3); + vec3 purple = vec3(0.5, 0.1, 0.8); + vec3 col = mix(green, purple, uv.y) * curtain * 2.0; + col += vec3(0.01, 0.0, 0.03); // night sky + fragColor = vec4(col, 1.0); +}""", ["aurora", "borealis", "nature", "night", "sky", "green", "purple", "atmospheric"], "2d", "Northern lights curtain with layered noise movement", +{"chaos_level": 0.3, "color_temperature": "cool", "motion_type": "fluid"}) + +s("Op Art Illusion", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float d = length(uv); + float a = atan(uv.y, uv.x); + float pattern = sin(d * 40.0 - iTime * 4.0) * sin(a * 12.0 + iTime * 2.0); + float bw = step(0.0, pattern); + float warp = sin(d * 20.0 + iTime) * 0.05; + bw = step(0.0, sin((d + warp) * 40.0 - iTime * 4.0) * sin(a * 12.0 + iTime * 2.0)); + fragColor = vec4(vec3(bw), 1.0); +}""", ["op-art", "illusion", "optical", "bw", "monochrome", "geometric", "hypnotic", "pattern"], "2d", "Black and white optical illusion with warping concentric rings", +{"chaos_level": 0.7, "color_temperature": "monochrome", "motion_type": "pulsing"}) + +s("Glitch Corruption", """ +float hash(float n) { return fract(sin(n) * 43758.5453); } +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float t = floor(iTime * 8.0); + float block = floor(uv.y * 20.0); + float shift = hash(block + t) * step(0.85, hash(block * 0.3 + t)); + uv.x += shift * 0.3 * (hash(t + block * 7.0) - 0.5); + vec3 col; + col.r = step(0.5, fract(sin(dot(uv + 0.01, vec2(12.9, 78.2))) * 43758.5)); + col.g = step(0.5, fract(sin(dot(uv, vec2(12.9, 78.2))) * 43758.5)); + col.b = step(0.5, fract(sin(dot(uv - 0.01, vec2(12.9, 78.2))) * 43758.5)); + float scanline = sin(uv.y * 500.0) * 0.04; + col += scanline; + fragColor = vec4(col, 1.0); +}""", ["glitch", "corruption", "digital", "error", "rgb-split", "cyberpunk", "broken"], "2d", "Data corruption with RGB channel splitting and block displacement", +{"chaos_level": 0.95, "color_temperature": "neutral", "motion_type": "chaotic"}) + +s("Sunset Gradient Waves", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float wave = sin(uv.x * 8.0 + iTime) * 0.03; + wave += sin(uv.x * 15.0 - iTime * 1.5) * 0.015; + float y = uv.y + wave; + vec3 sky1 = vec3(0.1, 0.0, 0.2); + vec3 sky2 = vec3(0.8, 0.2, 0.1); + vec3 sky3 = vec3(1.0, 0.7, 0.2); + vec3 col; + if (y > 0.6) col = mix(sky2, sky1, (y - 0.6) / 0.4); + else if (y > 0.3) col = mix(sky3, sky2, (y - 0.3) / 0.3); + else col = mix(vec3(1.0, 0.9, 0.5), sky3, y / 0.3); + float sun = smoothstep(0.12, 0.1, length(vec2(uv.x - 0.5, y - 0.45))); + col = mix(col, vec3(1.0, 0.95, 0.8), sun); + fragColor = vec4(col, 1.0); +}""", ["sunset", "gradient", "sky", "warm", "peaceful", "nature", "waves", "orange"], "2d", "Stylized sunset with layered gradient and gentle wave distortion", +{"chaos_level": 0.1, "color_temperature": "warm", "motion_type": "fluid"}) + +s("Hyperbolic Tiling", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float t = iTime * 0.3; + float scale = 1.0; + vec3 col = vec3(0.0); + for (int i = 0; i < 8; i++) { + uv = abs(uv) - 0.5; + uv *= 1.5; + scale *= 1.5; + float fi = float(i); + uv *= mat2(cos(t + fi), sin(t + fi), -sin(t + fi), cos(t + fi)); + } + float d = length(uv) / scale; + col = 0.5 + 0.5 * cos(d * 50.0 + iTime * 2.0 + vec3(0, 2, 4)); + fragColor = vec4(col, 1.0); +}""", ["hyperbolic", "tiling", "fractal", "recursive", "mathematical", "colorful", "abstract"], "2d", "Iterated function system creating hyperbolic-style tiling", +{"chaos_level": 0.7, "color_temperature": "neutral", "motion_type": "rotating"}) + +s("Liquid Metal", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), + mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float t = iTime * 0.3; + vec2 p = uv * 4.0; + float n = noise(p + t) + noise(p * 2.0 - t * 0.7) * 0.5 + noise(p * 4.0 + t * 0.3) * 0.25; + n = n / 1.75; + float env = pow(n, 1.5); + vec3 col = mix(vec3(0.3, 0.3, 0.35), vec3(0.9, 0.9, 0.95), env); + float spec = pow(max(0.0, n * 2.0 - 1.0), 8.0); + col += vec3(1.0) * spec * 0.5; + col = mix(col, col * vec3(0.8, 0.85, 1.0), 0.3); + fragColor = vec4(col, 1.0); +}""", ["metal", "liquid", "chrome", "silver", "reflective", "smooth", "elegant"], "2d", "Flowing liquid mercury surface with specular highlights", +{"chaos_level": 0.3, "color_temperature": "cool", "motion_type": "fluid"}) + +s("Firework Burst", """ +float hash(float n) { return fract(sin(n) * 43758.5453); } +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec3 col = vec3(0.0); + for (int b = 0; b < 5; b++) { + float fb = float(b); + float bt = mod(iTime + fb * 1.3, 4.0); + vec2 center = vec2(hash(fb * 7.0) - 0.5, hash(fb * 13.0) * 0.3 + 0.1) * 0.8; + float expand = bt * 0.4; + float fade = exp(-bt * 1.5); + for (int i = 0; i < 30; i++) { + float fi = float(i); + float angle = fi * 0.2094 + fb; + float speed = 0.8 + hash(fi + fb * 100.0) * 0.4; + vec2 pp = center + vec2(cos(angle), sin(angle)) * expand * speed; + pp.y -= bt * bt * 0.05; // gravity + float d = length(uv - pp); + vec3 sparkCol = 0.5 + 0.5 * cos(fb * 2.0 + vec3(0, 2, 4)); + col += sparkCol * 0.003 / (d + 0.001) * fade; + } + } + col = min(col, 1.0); + fragColor = vec4(col, 1.0); +}""", ["fireworks", "particles", "celebration", "explosion", "night", "colorful", "sparks"], "2d", "Multiple firework bursts with particle trails and gravity", +{"chaos_level": 0.7, "color_temperature": "warm", "motion_type": "explosive"}) + +s("Moiré Interference", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float d1 = length(uv - vec2(sin(iTime * 0.5) * 0.3, 0.0)); + float d2 = length(uv + vec2(sin(iTime * 0.5) * 0.3, 0.0)); + float p1 = sin(d1 * 60.0); + float p2 = sin(d2 * 60.0); + float moire = p1 * p2; + vec3 col = vec3(moire * 0.5 + 0.5); + fragColor = vec4(col, 1.0); +}""", ["moire", "interference", "optical", "monochrome", "geometric", "minimal", "waves"], "2d", "Two overlapping circular wave patterns creating moiré interference", +{"chaos_level": 0.4, "color_temperature": "monochrome", "motion_type": "oscillating"}) + +s("Electric Storm", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), + mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float t = iTime; + float bolt = 0.0; + for (int i = 0; i < 5; i++) { + float fi = float(i); + vec2 p = vec2(uv.x + noise(vec2(uv.y * 10.0 + t * 5.0, fi)) * 0.3, uv.y); + float d = abs(p.x - (fi - 2.0) * 0.15); + bolt += 0.005 / (d + 0.005) * step(0.7, noise(vec2(fi, floor(t * 3.0)))); + } + vec3 col = vec3(0.02, 0.0, 0.05); + col += vec3(0.3, 0.5, 1.0) * bolt; + col += vec3(0.8, 0.9, 1.0) * bolt * bolt; + fragColor = vec4(col, 1.0); +}""", ["lightning", "storm", "electric", "blue", "energy", "dramatic", "weather"], "2d", "Electric lightning bolts crackling across a dark sky", +{"chaos_level": 0.8, "color_temperature": "cool", "motion_type": "chaotic"}) + +s("Hexagonal Grid", """ +vec2 hexUV(vec2 uv) { + vec2 s = vec2(1.7320508, 1.0); + vec2 a = mod(uv, s) - s * 0.5; + vec2 b = mod(uv - s * 0.5, s) - s * 0.5; + return dot(a, a) < dot(b, b) ? a : b; +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec2 h = hexUV(uv * 8.0); + float d = length(h); + float edge = smoothstep(0.5, 0.48, d); + float inner = smoothstep(0.3, 0.28, d); + float pulse = sin(iTime * 2.0 + length(floor(uv * 8.0)) * 2.0) * 0.5 + 0.5; + vec3 col = vec3(0.05); + col += vec3(0.0, 0.6, 0.8) * (edge - inner) * 0.5; + col += vec3(0.0, 0.8, 1.0) * inner * pulse; + fragColor = vec4(col, 1.0); +}""", ["hexagon", "grid", "geometric", "teal", "tech", "clean", "pattern", "honeycomb"], "2d", "Pulsing hexagonal grid with teal neon glow", +{"chaos_level": 0.2, "color_temperature": "cool", "motion_type": "pulsing"}) + +# ═══════════════════════════════════════════════════════════ +# 3D — RAY MARCHING +# ═══════════════════════════════════════════════════════════ + +s("Infinite Tunnel", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float a = atan(uv.y, uv.x); + float r = length(uv); + float tunnel = 1.0 / r; + float tex = sin(a * 6.0 + tunnel * 2.0 - iTime * 3.0); + tex *= sin(tunnel * 4.0 - iTime * 2.0); + vec3 col = 0.5 + 0.5 * cos(tex * 3.0 + iTime + vec3(0, 2, 4)); + col *= smoothstep(0.0, 0.3, r) * (1.0 - smoothstep(0.8, 2.0, r)); + col *= 1.0 / (r * 3.0); + fragColor = vec4(clamp(col, 0.0, 1.0), 1.0); +}""", ["tunnel", "infinite", "warp", "psychedelic", "vortex", "3d-illusion", "trippy"], "3d", "Infinite tunnel flythrough with warping hexagonal texture", +{"chaos_level": 0.6, "color_temperature": "neutral", "motion_type": "forward"}) + +s("Raymarched Metaballs", """ +float sdSphere(vec3 p, float r) { return length(p) - r; } +float map(vec3 p) { + float d = 1e10; + for (int i = 0; i < 5; i++) { + float fi = float(i); + vec3 center = vec3( + sin(iTime + fi * 1.3) * 0.8, + cos(iTime * 0.7 + fi * 2.1) * 0.6, + sin(iTime * 0.5 + fi * 0.9) * 0.5 + ); + d = min(d, sdSphere(p - center, 0.4)); + } + // smooth union + return d; +} +vec3 getNormal(vec3 p) { + vec2 e = vec2(0.001, 0.0); + return normalize(vec3(map(p+e.xyy)-map(p-e.xyy), map(p+e.yxy)-map(p-e.yxy), map(p+e.yyx)-map(p-e.yyx))); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec3 ro = vec3(0, 0, -3); + vec3 rd = normalize(vec3(uv, 1.0)); + float t = 0.0; + for (int i = 0; i < 64; i++) { + float d = map(ro + rd * t); + if (d < 0.001 || t > 20.0) break; + t += d; + } + vec3 col = vec3(0.02, 0.01, 0.05); + if (t < 20.0) { + vec3 p = ro + rd * t; + vec3 n = getNormal(p); + vec3 light = normalize(vec3(1, 1, -1)); + float diff = max(dot(n, light), 0.0); + float spec = pow(max(dot(reflect(-light, n), -rd), 0.0), 32.0); + col = vec3(0.8, 0.2, 0.4) * diff + vec3(1.0) * spec * 0.5 + vec3(0.05, 0.02, 0.08); + } + fragColor = vec4(col, 1.0); +}""", ["metaballs", "3d", "raymarching", "organic", "blobs", "smooth", "pink"], "3d", "Five orbiting raymarched spheres with specular lighting", +{"chaos_level": 0.4, "color_temperature": "warm", "motion_type": "orbiting"}) + +s("Fractal Mountain Terrain", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), + mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); +} +float terrain(vec2 p) { + float h = 0.0, a = 0.5; + for (int i = 0; i < 6; i++) { h += a * noise(p); p *= 2.0; a *= 0.5; } + return h; +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec3 ro = vec3(iTime * 0.5, 1.5, 0.0); + vec3 rd = normalize(vec3(uv.x, uv.y - 0.2, 1.0)); + float t = 0.0; + vec3 col = vec3(0.4, 0.6, 0.9); // sky + for (int i = 0; i < 80; i++) { + vec3 p = ro + rd * t; + float h = terrain(p.xz) * 2.0; + if (p.y < h) { + vec3 n = normalize(vec3( + terrain(p.xz + vec2(0.01, 0)) - terrain(p.xz - vec2(0.01, 0)), + 0.02, + terrain(p.xz + vec2(0, 0.01)) - terrain(p.xz - vec2(0, 0.01)) + )); + float sun = max(dot(n, normalize(vec3(1, 0.8, 0.5))), 0.0); + col = mix(vec3(0.2, 0.3, 0.1), vec3(0.5, 0.4, 0.3), p.y / 2.0) * (0.3 + 0.7 * sun); + col = mix(col, vec3(0.4, 0.6, 0.9), 1.0 - exp(-t * 0.03)); + break; + } + t += 0.05 + t * 0.01; + } + fragColor = vec4(col, 1.0); +}""", ["terrain", "mountain", "landscape", "3d", "raymarching", "nature", "flyover", "procedural"], "3d", "Procedural fractal terrain with aerial flyover and atmospheric fog", +{"chaos_level": 0.3, "color_temperature": "neutral", "motion_type": "forward"}) + +s("Neon Torus Knot", """ +float sdTorus(vec3 p, vec2 t) { + vec2 q = vec2(length(p.xz) - t.x, p.y); + return length(q) - t.y; +} +float map(vec3 p) { + float a = iTime * 0.5; + p.xz *= mat2(cos(a), sin(a), -sin(a), cos(a)); + // trefoil knot via torus deformation + float r = length(p.xz); + float theta = atan(p.z, p.x); + vec3 q = vec3(r - 1.0, p.y, 0.0); + float knotAngle = theta * 3.0 + iTime; + q.xy -= 0.4 * vec2(cos(knotAngle), sin(knotAngle)); + return length(q) - 0.15; +} +vec3 getNormal(vec3 p) { + vec2 e = vec2(0.001, 0); + return normalize(vec3(map(p+e.xyy)-map(p-e.xyy), map(p+e.yxy)-map(p-e.yxy), map(p+e.yyx)-map(p-e.yyx))); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec3 ro = vec3(0, 0, -3.5); + vec3 rd = normalize(vec3(uv, 1.0)); + float t = 0.0; + vec3 col = vec3(0.01); + for (int i = 0; i < 80; i++) { + float d = map(ro + rd * t); + if (d < 0.001) { + vec3 p = ro + rd * t; + vec3 n = getNormal(p); + float fresnel = pow(1.0 - abs(dot(n, rd)), 3.0); + col = vec3(0.0, 0.8, 1.0) * (0.2 + 0.8 * fresnel); + col += vec3(1.0, 0.3, 0.8) * fresnel * fresnel; + float glow = 0.005 / (d + 0.005); + col += vec3(0.0, 0.4, 0.6) * glow; + break; + } + t += d; + if (t > 20.0) break; + // Accumulate glow from near-misses + col += vec3(0.0, 0.3, 0.5) * 0.002 / (d + 0.01); + } + fragColor = vec4(col, 1.0); +}""", ["torus", "knot", "3d", "neon", "raymarching", "glow", "cyan", "mathematical", "geometry"], "3d", "Raymarched trefoil torus knot with neon fresnel glow", +{"chaos_level": 0.4, "color_temperature": "cool", "motion_type": "rotating"}) + +s("Glass Refraction Orb", """ +float sdSphere(vec3 p, float r) { return length(p) - r; } +float map(vec3 p) { return sdSphere(p, 1.0); } +vec3 getNormal(vec3 p) { + vec2 e = vec2(0.001, 0); + return normalize(vec3(map(p+e.xyy)-map(p-e.xyy), map(p+e.yxy)-map(p-e.yxy), map(p+e.yyx)-map(p-e.yyx))); +} +vec3 background(vec3 rd) { + float t = iTime * 0.3; + return 0.5 + 0.5 * cos(rd * 5.0 + t + vec3(0, 2, 4)); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec3 ro = vec3(0, 0, -3); + vec3 rd = normalize(vec3(uv, 1.0)); + float t = 0.0; + for (int i = 0; i < 64; i++) { + float d = map(ro + rd * t); + if (d < 0.001) break; + t += d; + if (t > 20.0) break; + } + vec3 col; + if (t < 20.0) { + vec3 p = ro + rd * t; + vec3 n = getNormal(p); + float fresnel = pow(1.0 - abs(dot(n, rd)), 4.0); + vec3 refl = reflect(rd, n); + vec3 refr = refract(rd, n, 0.7); + col = mix(background(refr) * 0.8, background(refl), fresnel); + col += vec3(1.0) * pow(max(dot(refl, normalize(vec3(1, 1, -1))), 0.0), 64.0) * 0.5; + } else { + col = background(rd) * 0.3; + } + fragColor = vec4(col, 1.0); +}""", ["glass", "sphere", "refraction", "3d", "raymarching", "crystal", "transparent", "elegant"], "3d", "Transparent glass sphere with refraction and reflection", +{"chaos_level": 0.2, "color_temperature": "neutral", "motion_type": "static"}) + +# ═══════════════════════════════════════════════════════════ +# MORE 2D VARIETY +# ═══════════════════════════════════════════════════════════ + +s("Cellular Automata Flow", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float t = floor(iTime * 4.0); + vec2 cell = floor(uv * 40.0); + float alive = step(0.5, hash(cell + t * 0.01)); + float neighbors = 0.0; + for (int x = -1; x <= 1; x++) + for (int y = -1; y <= 1; y++) { + if (x == 0 && y == 0) continue; + neighbors += step(0.5, hash(cell + vec2(float(x), float(y)) + t * 0.01)); + } + float next = step(2.5, neighbors) * step(neighbors, 3.5) * (1.0 - alive) + + step(1.5, neighbors) * step(neighbors, 3.5) * alive; + vec3 col = mix(vec3(0.05, 0.0, 0.1), vec3(0.0, 0.8, 0.4), next); + fragColor = vec4(col, 1.0); +}""", ["cellular-automata", "conway", "life", "grid", "simulation", "green", "digital"], "2d", "Game of Life style cellular automata with hash-based state", +{"chaos_level": 0.6, "color_temperature": "cool", "motion_type": "evolving"}) + +s("Blackhole Gravity Lens", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float r = length(uv); + float a = atan(uv.y, uv.x); + float warp = 0.3 / (r + 0.01); + vec2 warped = vec2(cos(a + warp), sin(a + warp)) * (r + 0.1 / r); + float stars = step(0.98, fract(sin(dot(floor(warped * 20.0), vec2(127.1, 311.7))) * 43758.5453)); + float disk = smoothstep(0.4, 0.3, abs(warped.y * 5.0 / (length(warped) + 0.1))) + * smoothstep(0.1, 0.2, r) * smoothstep(0.8, 0.4, r); + vec3 col = vec3(0.0); + col += vec3(0.7, 0.5, 0.3) * disk * (sin(warped.x * 30.0 + iTime * 5.0) * 0.3 + 0.7); + col += vec3(0.8, 0.9, 1.0) * stars * (1.0 - smoothstep(0.0, 0.15, r)); + col += vec3(0.8, 0.9, 1.0) * stars * smoothstep(0.3, 0.5, r); + float shadow = smoothstep(0.12, 0.1, r); + col *= 1.0 - shadow; + fragColor = vec4(col, 1.0); +}""", ["blackhole", "space", "gravity", "lens", "accretion", "cosmic", "sci-fi", "dark"], "2d", "Gravitational lensing around a black hole with accretion disk", +{"chaos_level": 0.5, "color_temperature": "warm", "motion_type": "rotating"}) + +s("Waveform Oscilloscope", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + uv.y = uv.y * 2.0 - 1.0; + float wave = sin(uv.x * 25.0 + iTime * 5.0) * 0.3; + wave += sin(uv.x * 13.0 - iTime * 3.0) * 0.15; + wave += sin(uv.x * 47.0 + iTime * 8.0) * 0.05; + float d = abs(uv.y - wave); + float line = 0.003 / d; + float glow = 0.01 / d; + vec3 col = vec3(0.0); + col += vec3(0.0, 1.0, 0.3) * line; + col += vec3(0.0, 0.4, 0.15) * glow; + float grid = step(0.99, fract(uv.x * 10.0)) + step(0.99, fract((uv.y * 0.5 + 0.5) * 5.0)); + col += vec3(0.03) * grid; + fragColor = vec4(col, 1.0); +}""", ["waveform", "oscilloscope", "audio", "signal", "green", "tech", "minimal", "retro"], "2d", "Animated oscilloscope display with composite waveform", +{"chaos_level": 0.3, "color_temperature": "cool", "motion_type": "oscillating"}) + +s("Sacred Geometry Flower", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float r = length(uv); + float a = atan(uv.y, uv.x); + float petals = abs(sin(a * 6.0 + iTime * 0.5)) * 0.3 + 0.2; + float flower = smoothstep(petals + 0.01, petals, r); + float inner = smoothstep(0.08, 0.06, r); + float rings = sin(r * 60.0 - iTime * 3.0) * 0.5 + 0.5; + rings *= smoothstep(0.5, 0.1, r); + vec3 col = vec3(0.02, 0.0, 0.04); + col += vec3(0.8, 0.4, 0.1) * flower * rings; + col += vec3(1.0, 0.8, 0.3) * inner; + col += vec3(0.3, 0.0, 0.5) * (1.0 - flower) * rings * 0.3; + fragColor = vec4(col, 1.0); +}""", ["sacred-geometry", "flower", "mandala", "spiritual", "gold", "symmetry", "organic"], "2d", "Sacred geometry flower of life with golden petal animation", +{"chaos_level": 0.2, "color_temperature": "warm", "motion_type": "breathing"}) + +s("Pixel Art Dither", """ +float dither(vec2 p, float v) { + mat4 bayer = mat4( + 0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0, + 12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0, + 3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0, + 15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0 + ); + int x = int(mod(p.x, 4.0)); + int y = int(mod(p.y, 4.0)); + float threshold = bayer[y][x]; + return step(threshold, v); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + vec2 pixel = floor(fragCoord / 4.0); + float t = iTime * 0.5; + float wave = sin(uv.x * 5.0 + t) * sin(uv.y * 3.0 + t * 0.7) * 0.5 + 0.5; + float d = dither(pixel, wave); + vec3 dark = vec3(0.08, 0.04, 0.15); + vec3 light = vec3(0.3, 0.8, 0.6); + vec3 col = mix(dark, light, d); + fragColor = vec4(col, 1.0); +}""", ["pixel-art", "dither", "retro", "8bit", "bayer", "low-fi", "nostalgic"], "2d", "Bayer matrix ordered dithering with animated value field", +{"chaos_level": 0.3, "color_temperature": "cool", "motion_type": "pulsing"}) + +s("Reaction Diffusion", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), + mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float t = iTime * 0.1; + float n = 0.0; + float scale = 8.0; + n += noise(uv * scale + t); + n += noise(uv * scale * 2.0 + sin(t * 3.0)) * 0.5; + n += noise(uv * scale * 4.0 - t * 2.0) * 0.25; + n = sin(n * 8.0) * 0.5 + 0.5; + float spots = smoothstep(0.4, 0.6, n); + vec3 col = mix(vec3(0.9, 0.85, 0.7), vec3(0.15, 0.1, 0.05), spots); + fragColor = vec4(col, 1.0); +}""", ["reaction-diffusion", "organic", "pattern", "turing", "biological", "spots", "nature"], "2d", "Turing-style reaction diffusion patterns like animal markings", +{"chaos_level": 0.4, "color_temperature": "warm", "motion_type": "morphing"}) + +s("Tron Light Cycle Trail", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + vec3 col = vec3(0.02, 0.02, 0.05); + // Perspective grid floor + float gy = 0.5 / (1.0 - uv.y + 0.001); + float gx = (uv.x - 0.5) * gy * 2.0; + float gridX = smoothstep(0.02, 0.0, abs(fract(gx + iTime * 0.5) - 0.5)); + float gridZ = smoothstep(0.02, 0.0, abs(fract(gy * 0.5 - iTime * 2.0) - 0.5)); + float grid = max(gridX, gridZ) * step(0.5, uv.y) * (1.0 - uv.y) * 4.0; + col += vec3(0.0, 0.5, 1.0) * grid * 0.5; + // Light trail + float trail = smoothstep(0.01, 0.0, abs(uv.x - 0.5 - sin(iTime) * 0.2)); + trail *= step(0.5, uv.y); + col += vec3(0.0, 0.8, 1.0) * trail * 2.0; + fragColor = vec4(col, 1.0); +}""", ["tron", "grid", "synthwave", "cyberpunk", "blue", "neon", "retro-future", "perspective"], "2d", "Tron-style perspective grid with light cycle trail", +{"chaos_level": 0.2, "color_temperature": "cool", "motion_type": "forward"}) + +s("Watercolor Bleed", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), + mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); +} +float fbm(vec2 p) { + float v = 0.0, a = 0.5; + mat2 rot = mat2(0.8, 0.6, -0.6, 0.8); + for (int i = 0; i < 6; i++) { v += a * noise(p); p = rot * p * 2.0; a *= 0.5; } + return v; +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float t = iTime * 0.05; + float f = fbm(uv * 4.0 + t); + vec3 col1 = vec3(0.9, 0.3, 0.2); + vec3 col2 = vec3(0.2, 0.5, 0.9); + vec3 col3 = vec3(0.1, 0.8, 0.4); + vec3 col = mix(col1, col2, smoothstep(0.3, 0.5, f)); + col = mix(col, col3, smoothstep(0.5, 0.7, f)); + col = mix(col, vec3(0.95, 0.92, 0.88), smoothstep(0.7, 0.9, f)); + // paper texture + float paper = noise(uv * 200.0) * 0.05 + 0.95; + col *= paper; + fragColor = vec4(col, 1.0); +}""", ["watercolor", "paint", "artistic", "soft", "pastel", "organic", "paper", "art"], "2d", "Watercolor paint bleeding on textured paper", +{"chaos_level": 0.3, "color_temperature": "warm", "motion_type": "fluid"}) + +# ═══════════════════════════════════════════════════════════ +# PROGRAMMATIC GENERATION — fill to 200+ +# We generate families of variations mathematically +# ═══════════════════════════════════════════════════════════ + +# --- Family: Sine field variations (20 shaders) --- +sine_colors = [ + ("Crimson Pulse", "vec3(0.9, 0.1, 0.15)", "vec3(0.3, 0.0, 0.05)", ["red", "pulse", "warm", "intense"]), + ("Ocean Depth", "vec3(0.0, 0.3, 0.9)", "vec3(0.0, 0.05, 0.15)", ["ocean", "blue", "deep", "calm"]), + ("Toxic Glow", "vec3(0.2, 1.0, 0.1)", "vec3(0.0, 0.1, 0.0)", ["toxic", "green", "radioactive", "glow"]), + ("Amber Warmth", "vec3(1.0, 0.6, 0.1)", "vec3(0.15, 0.05, 0.0)", ["amber", "warm", "golden", "honey"]), + ("Violet Dream", "vec3(0.6, 0.1, 0.9)", "vec3(0.1, 0.0, 0.15)", ["violet", "dream", "purple", "ethereal"]), + ("Ice Crystal", "vec3(0.7, 0.9, 1.0)", "vec3(0.05, 0.08, 0.12)", ["ice", "crystal", "frozen", "cold"]), + ("Coral Reef", "vec3(1.0, 0.4, 0.5)", "vec3(0.1, 0.02, 0.05)", ["coral", "reef", "pink", "organic"]), + ("Forest Canopy", "vec3(0.1, 0.6, 0.2)", "vec3(0.02, 0.08, 0.02)", ["forest", "green", "nature", "canopy"]), + ("Copper Wire", "vec3(0.9, 0.5, 0.2)", "vec3(0.1, 0.03, 0.0)", ["copper", "metal", "wire", "industrial"]), + ("Midnight Blue", "vec3(0.1, 0.15, 0.5)", "vec3(0.01, 0.01, 0.05)", ["midnight", "blue", "dark", "night"]), + ("Sunrise Ember", "vec3(1.0, 0.3, 0.0)", "vec3(0.2, 0.02, 0.0)", ["sunrise", "ember", "fire", "dawn"]), + ("Lavender Haze", "vec3(0.7, 0.5, 0.9)", "vec3(0.08, 0.05, 0.12)", ["lavender", "haze", "soft", "pastel"]), + ("Steel Grey", "vec3(0.5, 0.52, 0.55)", "vec3(0.05, 0.05, 0.06)", ["steel", "grey", "industrial", "monochrome"]), + ("Magma Core", "vec3(1.0, 0.2, 0.0)", "vec3(0.3, 0.0, 0.0)", ["magma", "lava", "volcanic", "hot"]), + ("Teal Abyss", "vec3(0.0, 0.7, 0.6)", "vec3(0.0, 0.08, 0.07)", ["teal", "abyss", "aqua", "deep"]), + ("Champagne", "vec3(0.95, 0.85, 0.6)", "vec3(0.12, 0.1, 0.05)", ["champagne", "gold", "luxury", "elegant"]), + ("Electric Lime", "vec3(0.6, 1.0, 0.0)", "vec3(0.05, 0.12, 0.0)", ["electric", "lime", "acid", "bright"]), + ("Blood Moon", "vec3(0.6, 0.05, 0.05)", "vec3(0.1, 0.0, 0.02)", ["blood", "moon", "dark-red", "ominous"]), + ("Arctic Wind", "vec3(0.85, 0.95, 1.0)", "vec3(0.08, 0.1, 0.15)", ["arctic", "wind", "white", "cold"]), + ("Rust Decay", "vec3(0.6, 0.3, 0.1)", "vec3(0.08, 0.03, 0.01)", ["rust", "decay", "brown", "aged"]), +] + +for i, (name, bright, dark, extra_tags) in enumerate(sine_colors): + freq_x = 5 + (i % 7) * 3 + freq_y = 7 + (i % 5) * 4 + speed = 0.5 + (i % 4) * 0.3 + complexity = ["sin", "cos", "sin+cos"][i % 3] + + code = f"""void mainImage(out vec4 fragColor, in vec2 fragCoord) {{ + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float t = iTime * {speed:.1f}; + float v = sin(uv.x * {freq_x:.1f} + t) * cos(uv.y * {freq_y:.1f} - t * 0.7); + v += sin(length(uv) * {freq_x + freq_y:.1f} + t * 1.3) * 0.5; + v = v * 0.5 + 0.5; + v = pow(v, 1.5); + vec3 col = mix({dark}, {bright}, v); + fragColor = vec4(col, 1.0); +}}""" + s(name, code, + ["sine-field", "mathematical", "animated"] + extra_tags, + "2d", f"Sine wave interference pattern in {name.lower()} palette", + {"chaos_level": 0.3 + (i % 5) * 0.1, "color_temperature": ["warm", "cool", "neutral"][i % 3], "motion_type": "pulsing"}) + +# --- Family: Rotating geometry (15 shaders) --- +geometries = [ + ("Spinning Cube Wireframe", 4, "vec3(0.0, 0.8, 1.0)", ["cube", "wireframe", "3d-illusion", "blue"]), + ("Pentagonal Vortex", 5, "vec3(0.9, 0.3, 0.8)", ["pentagon", "vortex", "pink"]), + ("Triangular Recursion", 3, "vec3(1.0, 0.8, 0.0)", ["triangle", "recursive", "yellow"]), + ("Octagonal Mandala", 8, "vec3(0.3, 0.9, 0.5)", ["octagon", "mandala", "green"]), + ("Star Field Rotation", 5, "vec3(0.9, 0.9, 1.0)", ["star", "rotation", "white"]), + ("Decagonal Wave", 10, "vec3(0.8, 0.4, 0.0)", ["decagon", "wave", "orange"]), + ("Heptagonal Bloom", 7, "vec3(0.5, 0.0, 0.9)", ["heptagon", "bloom", "purple"]), + ("Square Spiral", 4, "vec3(1.0, 0.2, 0.3)", ["square", "spiral", "red"]), + ("Hexagonal Pulse", 6, "vec3(0.0, 0.9, 0.7)", ["hexagon", "pulse", "teal"]), + ("Nonagonal Flow", 9, "vec3(0.9, 0.7, 0.3)", ["nonagon", "flow", "gold"]), + ("Diamond Lattice", 4, "vec3(0.6, 0.8, 1.0)", ["diamond", "lattice", "crystal"]), + ("Trigon Collapse", 3, "vec3(1.0, 0.0, 0.5)", ["triangle", "collapse", "magenta"]), + ("Polygon Storm", 12, "vec3(0.4, 0.4, 0.9)", ["polygon", "storm", "blue"]), + ("Angular Meditation", 6, "vec3(0.9, 0.85, 0.7)", ["angular", "meditation", "calm"]), + ("Vertex Dance", 8, "vec3(0.0, 1.0, 0.4)", ["vertex", "dance", "neon-green"]), +] + +for i, (name, sides, color, extra_tags) in enumerate(geometries): + speed = 0.5 + (i % 3) * 0.3 + layers = 3 + i % 4 + + code = f"""void mainImage(out vec4 fragColor, in vec2 fragCoord) {{ + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float t = iTime * {speed:.1f}; + vec3 col = vec3(0.02); + for (int layer = 0; layer < {layers}; layer++) {{ + float fl = float(layer); + float scale = 1.0 + fl * 0.5; + float rot = t + fl * 0.5; + vec2 p = uv * scale; + p = vec2(p.x * cos(rot) - p.y * sin(rot), p.x * sin(rot) + p.y * cos(rot)); + float a = atan(p.y, p.x); + float r = length(p); + float sides = {float(sides):.1f}; + float polygon = cos(3.14159 / sides) / cos(mod(a + 3.14159 / sides, 2.0 * 3.14159 / sides) - 3.14159 / sides); + float edge = abs(r - polygon * (0.3 + fl * 0.1)); + float line = 0.003 / (edge + 0.003); + col += {color} * line * (0.5 + 0.5 / (1.0 + fl)); + }} + fragColor = vec4(col, 1.0); +}}""" + s(name, code, + ["geometric", "rotating", "polygon", "animated", "line-art"] + extra_tags, + "2d", f"Layered rotating {sides}-sided polygon wireframes", + {"chaos_level": 0.3 + (i % 4) * 0.1, "color_temperature": ["cool", "warm", "neutral"][i % 3], "motion_type": "rotating"}) + +# --- Family: Noise landscape variations (15 shaders) --- +landscapes = [ + ("Desert Dunes", "vec3(0.9, 0.7, 0.4)", "vec3(0.4, 0.6, 0.9)", 0.3, ["desert", "dunes", "sand", "warm"]), + ("Frozen Tundra", "vec3(0.85, 0.9, 0.95)", "vec3(0.3, 0.4, 0.5)", 0.15, ["frozen", "tundra", "ice", "cold"]), + ("Volcanic Plains", "vec3(0.3, 0.1, 0.05)", "vec3(0.1, 0.0, 0.0)", 0.8, ["volcanic", "lava", "dark", "ominous"]), + ("Rolling Hills", "vec3(0.2, 0.5, 0.15)", "vec3(0.5, 0.7, 0.9)", 0.4, ["hills", "green", "pastoral", "peaceful"]), + ("Alien Terrain", "vec3(0.4, 0.0, 0.5)", "vec3(0.1, 0.8, 0.3)", 0.6, ["alien", "sci-fi", "surreal", "otherworldly"]), + ("Oceanic Floor", "vec3(0.05, 0.15, 0.25)", "vec3(0.0, 0.3, 0.5)", 0.35, ["ocean-floor", "underwater", "deep", "blue"]), + ("Mars Surface", "vec3(0.7, 0.3, 0.1)", "vec3(0.3, 0.15, 0.1)", 0.5, ["mars", "planet", "red", "space"]), + ("Crystal Cavern", "vec3(0.3, 0.5, 0.7)", "vec3(0.1, 0.0, 0.2)", 0.55, ["crystal", "cavern", "underground", "sparkle"]), + ("Cloud Tops", "vec3(0.95, 0.95, 1.0)", "vec3(0.3, 0.5, 0.8)", 0.2, ["clouds", "sky", "white", "fluffy"]), + ("Moss Garden", "vec3(0.15, 0.35, 0.1)", "vec3(0.3, 0.4, 0.3)", 0.25, ["moss", "garden", "zen", "green"]), + ("Rust Belt", "vec3(0.5, 0.25, 0.1)", "vec3(0.2, 0.15, 0.1)", 0.45, ["rust", "industrial", "decay", "brown"]), + ("Nebula Gas", "vec3(0.6, 0.2, 0.8)", "vec3(0.05, 0.0, 0.1)", 0.7, ["nebula", "gas", "space", "purple"]), + ("Coral Depths", "vec3(0.9, 0.4, 0.3)", "vec3(0.0, 0.1, 0.2)", 0.4, ["coral", "ocean", "underwater", "warm"]), + ("Storm Front", "vec3(0.2, 0.22, 0.25)", "vec3(0.05, 0.05, 0.08)", 0.65, ["storm", "dark", "weather", "dramatic"]), + ("Golden Hour", "vec3(1.0, 0.75, 0.4)", "vec3(0.3, 0.2, 0.4)", 0.3, ["golden-hour", "warm", "photography", "sunset"]), +] + +for i, (name, ground, sky, roughness, extra_tags) in enumerate(landscapes): + octaves = 5 + i % 3 + speed = 0.1 + (i % 4) * 0.1 + code = f"""float hash(vec2 p) {{ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }} +float noise(vec2 p) {{ + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), + mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); +}} +void mainImage(out vec4 fragColor, in vec2 fragCoord) {{ + vec2 uv = fragCoord / iResolution.xy; + float t = iTime * {speed:.2f}; + vec2 p = uv * 5.0 + vec2(t, 0.0); + float h = 0.0, a = 0.5; + for (int i = 0; i < {octaves}; i++) {{ h += a * noise(p); p *= 2.0; a *= 0.5; }} + float terrain = smoothstep(0.3, 0.7, h + (uv.y - 0.5) * {roughness + 0.5:.1f}); + vec3 col = mix({ground}, {sky}, terrain); + float fog = smoothstep(0.0, 1.0, uv.y); + col = mix(col, {sky} * 0.7, fog * 0.3); + fragColor = vec4(col, 1.0); +}}""" + s(name, code, + ["landscape", "terrain", "noise", "procedural"] + extra_tags, + "2d", f"Procedural {name.lower()} landscape with {octaves}-octave noise", + {"chaos_level": roughness, "color_temperature": ["warm", "cool", "neutral"][i % 3], "motion_type": "scrolling"}) + +# --- Family: 3D raymarched primitives (15 shaders) --- +rm_shapes = [ + ("Warped Cube", "max(max(abs(p.x),abs(p.y)),abs(p.z))-0.8+sin(p.x*5.0+iTime)*0.1", "vec3(0.9,0.3,0.1)", ["cube", "warped", "organic"]), + ("Spinning Top", "length(vec2(length(p.xz)-0.6,p.y))-0.2", "vec3(0.1,0.7,0.9)", ["torus", "spinning", "blue"]), + ("Infinite Pillars", "length(mod(p.xz+1.0,2.0)-1.0)-0.3", "vec3(0.8,0.8,0.85)", ["pillars", "infinite", "architectural"]), + ("Twisted Column", "length(vec2(length(p.xz*mat2(cos(p.y),sin(p.y),-sin(p.y),cos(p.y)))-0.5,0.0))-0.15", "vec3(0.6,0.2,0.8)", ["twisted", "column", "purple"]), + ("Gyroid Surface", "sin(p.x)*cos(p.y)+sin(p.y)*cos(p.z)+sin(p.z)*cos(p.x)", "vec3(0.2,0.8,0.4)", ["gyroid", "minimal-surface", "mathematical"]), + ("Sierpinski Tetra", "max(max(-p.x-p.y-p.z,p.x+p.y-p.z),max(-p.x+p.y+p.z,p.x-p.y+p.z))-1.0", "vec3(1.0,0.8,0.2)", ["sierpinski", "fractal", "gold"]), + ("Rounded Box", "length(max(abs(p)-vec3(0.6,0.4,0.3),0.0))-0.1", "vec3(0.3,0.5,0.9)", ["box", "rounded", "smooth"]), + ("Egg Shape", "length(p*vec3(1.0,1.3,1.0))-0.8", "vec3(0.95,0.9,0.8)", ["egg", "organic", "smooth", "minimal"]), + ("Capped Cylinder", "max(length(p.xz)-0.4,abs(p.y)-1.0)", "vec3(0.7,0.3,0.3)", ["cylinder", "geometric", "red"]), + ("Octahedron", "(abs(p.x)+abs(p.y)+abs(p.z)-1.0)*0.577", "vec3(0.4,0.9,0.7)", ["octahedron", "platonic", "teal"]), + ("Capsule Link", "length(p-vec3(0,clamp(p.y,-0.5,0.5),0))-0.3", "vec3(0.9,0.5,0.2)", ["capsule", "simple", "orange"]), + ("Cross Shape", "min(min(length(p.xy)-0.2,length(p.yz)-0.2),length(p.xz)-0.2)", "vec3(0.8,0.1,0.1)", ["cross", "intersection", "red"]), + ("Pulsing Heart", "pow(p.x*p.x+0.9*p.y*p.y+p.z*p.z-1.0,3.0)-p.x*p.x*p.y*p.y*p.y-0.1*p.z*p.z*p.y*p.y*p.y", "vec3(0.9,0.15,0.2)", ["heart", "love", "romantic"]), + ("Klein Bottle", "length(vec2(length(p.xy)-1.0,p.z))-0.3", "vec3(0.5,0.7,1.0)", ["klein-bottle", "topology", "mathematical"]), + ("Menger Sponge", "max(max(abs(p.x),abs(p.y)),abs(p.z))-1.0", "vec3(0.6,0.6,0.65)", ["menger", "sponge", "fractal", "recursive"]), +] + +for i, (name, sdf, color, extra_tags) in enumerate(rm_shapes): + rot_speed = 0.3 + (i % 4) * 0.2 + code = f"""float map(vec3 p) {{ + float a = iTime * {rot_speed:.1f}; + p.xz *= mat2(cos(a), sin(a), -sin(a), cos(a)); + p.xy *= mat2(cos(a*0.7), sin(a*0.7), -sin(a*0.7), cos(a*0.7)); + return {sdf}; +}} +vec3 getNormal(vec3 p) {{ + vec2 e = vec2(0.001, 0); + return normalize(vec3(map(p+e.xyy)-map(p-e.xyy),map(p+e.yxy)-map(p-e.yxy),map(p+e.yyx)-map(p-e.yyx))); +}} +void mainImage(out vec4 fragColor, in vec2 fragCoord) {{ + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec3 ro = vec3(0, 0, -3.5); + vec3 rd = normalize(vec3(uv, 1.0)); + float t = 0.0; + vec3 col = vec3(0.02, 0.01, 0.04); + for (int i = 0; i < 80; i++) {{ + float d = map(ro + rd * t); + if (abs(d) < 0.001) {{ + vec3 p = ro + rd * t; + vec3 n = getNormal(p); + vec3 light = normalize(vec3(1, 1, -1)); + float diff = max(dot(n, light), 0.0); + float spec = pow(max(dot(reflect(-light, n), -rd), 0.0), 16.0); + col = {color} * (0.15 + 0.85 * diff) + vec3(1.0) * spec * 0.3; + break; + }} + t += abs(d) * 0.8; + if (t > 20.0) break; + }} + fragColor = vec4(col, 1.0); +}}""" + s(name, code, + ["3d", "raymarching", "sdf", "animated"] + extra_tags, + "3d", f"Raymarched {name.lower()} with dual-axis rotation and specular lighting", + {"chaos_level": 0.3 + (i % 5) * 0.1, "color_temperature": ["cool", "warm", "neutral"][i % 3], "motion_type": "rotating"}) + +# --- Family: Abstract energy fields (15 shaders) --- +energies = [ + ("Plasma Core", 8.0, 3.0, "vec3(1.0,0.3,0.1)", "vec3(0.3,0.1,0.8)", ["plasma", "core", "energy"]), + ("Neural Network", 15.0, 5.0, "vec3(0.0,0.8,0.9)", "vec3(0.0,0.2,0.3)", ["neural", "network", "connections"]), + ("Magnetic Field", 6.0, 2.0, "vec3(0.2,0.5,1.0)", "vec3(0.8,0.2,0.1)", ["magnetic", "field", "physics"]), + ("Solar Flare", 4.0, 6.0, "vec3(1.0,0.7,0.0)", "vec3(1.0,0.1,0.0)", ["solar", "flare", "sun"]), + ("Quantum Foam", 20.0, 8.0, "vec3(0.5,0.8,1.0)", "vec3(0.1,0.0,0.2)", ["quantum", "foam", "microscopic"]), + ("Nebula Birth", 3.0, 1.5, "vec3(0.8,0.3,0.9)", "vec3(0.1,0.5,0.8)", ["nebula", "birth", "cosmic"]), + ("Lava Flow", 5.0, 2.0, "vec3(1.0,0.4,0.0)", "vec3(0.3,0.0,0.0)", ["lava", "flow", "volcanic"]), + ("Energy Shield", 10.0, 4.0, "vec3(0.0,0.9,0.5)", "vec3(0.0,0.2,0.1)", ["shield", "force-field", "sci-fi"]), + ("Radioactive Decay", 12.0, 7.0, "vec3(0.3,1.0,0.0)", "vec3(0.0,0.15,0.0)", ["radioactive", "decay", "toxic"]), + ("Dark Matter", 7.0, 3.0, "vec3(0.15,0.1,0.2)", "vec3(0.4,0.3,0.5)", ["dark-matter", "mysterious", "space"]), + ("Bioluminescent", 9.0, 4.0, "vec3(0.0,0.7,0.9)", "vec3(0.0,0.3,0.1)", ["bioluminescent", "ocean", "glow"]), + ("Cosmic Web", 6.0, 5.0, "vec3(0.6,0.7,1.0)", "vec3(0.05,0.0,0.1)", ["cosmic", "web", "universe"]), + ("Aurora Pulse", 8.0, 3.0, "vec3(0.1,0.9,0.4)", "vec3(0.4,0.1,0.8)", ["aurora", "pulse", "atmospheric"]), + ("Plasma Tornado", 5.0, 6.0, "vec3(0.9,0.5,1.0)", "vec3(0.2,0.0,0.3)", ["tornado", "vortex", "spinning"]), + ("Star Forge", 4.0, 4.0, "vec3(1.0,0.9,0.5)", "vec3(0.5,0.1,0.0)", ["star", "forge", "creation"]), +] + +for i, (name, freq, speed, col1, col2, extra_tags) in enumerate(energies): + code = f"""float hash(vec2 p) {{ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }} +float noise(vec2 p) {{ + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), + mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); +}} +void mainImage(out vec4 fragColor, in vec2 fragCoord) {{ + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float t = iTime * {speed / 3.0:.2f}; + float r = length(uv); + float a = atan(uv.y, uv.x); + float n = noise(vec2(a * {freq:.1f} / 6.283, r * {freq:.1f}) + t); + n += noise(vec2(a * {freq * 2:.1f} / 6.283, r * {freq * 2:.1f}) - t * 1.3) * 0.5; + n = n / 1.5; + float energy = pow(n, 2.0) * exp(-r * 2.0); + vec3 col = mix({col2}, {col1}, energy * 2.0); + col += {col1} * 0.01 / (r + 0.01) * n; + fragColor = vec4(col, 1.0); +}}""" + s(name, code, + ["energy", "abstract", "animated", "glow"] + extra_tags, + "2d", f"Swirling energy field: {name.lower()}", + {"chaos_level": 0.4 + (i % 5) * 0.1, "color_temperature": ["warm", "cool", "neutral"][i % 3], "motion_type": "swirling"}) + +# --- Family: Fractal Explorations (15 shaders) --- +fractals = [ + ("Julia Set Amethyst", -0.7269, 0.1889, "vec3(0.5,0.2,0.8)", ["julia", "amethyst", "fractal"]), + ("Julia Set Coral", -0.8, 0.156, "vec3(0.9,0.4,0.3)", ["julia", "coral", "fractal"]), + ("Julia Set Ice", -0.4, 0.6, "vec3(0.5,0.8,1.0)", ["julia", "ice", "fractal"]), + ("Julia Set Fire", -0.12, 0.74, "vec3(1.0,0.4,0.0)", ["julia", "fire", "fractal"]), + ("Julia Set Forest", 0.285, 0.01, "vec3(0.2,0.7,0.3)", ["julia", "forest", "fractal"]), + ("Julia Set Electric", -0.74543, 0.11301, "vec3(0.0,0.6,1.0)", ["julia", "electric", "fractal"]), + ("Julia Set Midnight", -0.75, 0.11, "vec3(0.15,0.1,0.3)", ["julia", "midnight", "fractal"]), + ("Julia Set Gold", -0.1, 0.65, "vec3(0.9,0.7,0.2)", ["julia", "gold", "fractal"]), + ("Julia Set Rose", -0.7, 0.27015, "vec3(0.9,0.3,0.5)", ["julia", "rose", "fractal"]), + ("Julia Set Nebula", 0.355, 0.355, "vec3(0.6,0.3,0.9)", ["julia", "nebula", "fractal"]), + ("Julia Set Ocean", -0.75, 0.0, "vec3(0.1,0.5,0.8)", ["julia", "ocean", "fractal"]), + ("Julia Set Ember", -0.77, 0.22, "vec3(0.8,0.2,0.05)", ["julia", "ember", "fractal"]), + ("Julia Set Toxic", -0.8, 0.0, "vec3(0.3,0.9,0.1)", ["julia", "toxic", "fractal"]), + ("Julia Set Void", 0.0, 0.65, "vec3(0.3,0.3,0.35)", ["julia", "void", "fractal"]), + ("Julia Set Prism", -0.7, 0.3, "vec3(0.7,0.5,1.0)", ["julia", "prism", "fractal"]), +] + +for i, (name, cx, cy, color, extra_tags) in enumerate(fractals): + code = f"""void mainImage(out vec4 fragColor, in vec2 fragCoord) {{ + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + uv *= 2.5; + vec2 c = vec2({cx}, {cy}) + vec2(sin(iTime * 0.1) * 0.02, cos(iTime * 0.13) * 0.02); + vec2 z = uv; + int iter = 0; + for (int i = 0; i < 200; i++) {{ + z = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + c; + if (dot(z, z) > 4.0) break; + iter = i; + }} + float t = float(iter) / 200.0; + vec3 col = {color} * (1.0 - pow(t, 0.5)); + col += 0.5 + 0.5 * cos(t * 12.0 + iTime + vec3(0, 2, 4)) * (1.0 - t); + col *= 1.0 - step(199.0, float(iter)); + fragColor = vec4(col, 1.0); +}}""" + s(name, code, + ["fractal", "julia-set", "mathematical", "complex", "colorful"] + extra_tags, + "2d", f"Julia set with c=({cx}, {cy}), slowly drifting", + {"chaos_level": 0.5 + (i % 4) * 0.1, "color_temperature": ["warm", "cool", "neutral"][i % 3], "motion_type": "morphing"}) + +# --- Family: Pattern/texture generators (15 shaders) --- +patterns = [ + ("Checkerboard Warp", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float warp = sin(length(uv) * 10.0 - iTime * 2.0) * 0.1; + uv += uv * warp; + vec2 grid = floor(uv * 8.0); + float check = mod(grid.x + grid.y, 2.0); + vec3 col = mix(vec3(0.1), vec3(0.9), check); + fragColor = vec4(col, 1.0); +}""", ["checkerboard", "warp", "distortion", "bw", "optical"]), + ("Concentric Rings", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float r = length(uv); + float rings = sin(r * 30.0 - iTime * 3.0) * 0.5 + 0.5; + vec3 col = mix(vec3(0.05, 0.0, 0.1), vec3(0.8, 0.3, 0.9), rings); + col *= smoothstep(1.5, 0.0, r); + fragColor = vec4(col, 1.0); +}""", ["concentric", "rings", "purple", "hypnotic", "simple"]), + ("Herringbone", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + vec2 g = uv * vec2(20.0, 40.0); + float row = floor(g.y); + g.x += mod(row, 2.0) * 0.5; + vec2 f = fract(g); + float brick = step(0.05, f.x) * step(0.05, f.y); + float t = sin(iTime + row * 0.3) * 0.5 + 0.5; + vec3 col = mix(vec3(0.6, 0.3, 0.15), vec3(0.7, 0.4, 0.2), t) * brick; + col += vec3(0.3, 0.15, 0.05) * (1.0 - brick); + fragColor = vec4(col, 1.0); +}""", ["herringbone", "brick", "pattern", "architecture", "warm"]), + ("Dots Matrix", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + vec2 grid = fract(uv * 20.0) - 0.5; + vec2 id = floor(uv * 20.0); + float phase = sin(iTime * 2.0 + id.x * 0.5 + id.y * 0.7); + float radius = 0.2 + 0.15 * phase; + float d = smoothstep(radius, radius - 0.02, length(grid)); + vec3 col = vec3(0.0, 0.5, 0.8) * d; + fragColor = vec4(col, 1.0); +}""", ["dots", "matrix", "halftone", "blue", "pulsing"]), + ("Weave Pattern", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + vec2 g = uv * 15.0; + float weft = step(0.3, fract(g.x)) * step(fract(g.x), 0.7); + float warp = step(0.3, fract(g.y)) * step(fract(g.y), 0.7); + float over = step(0.5, fract(floor(g.x) * 0.5 + floor(g.y) * 0.5)); + float thread = mix(weft, warp, over); + vec3 col1 = vec3(0.2, 0.3, 0.6); + vec3 col2 = vec3(0.7, 0.5, 0.2); + vec3 col = mix(col1, col2, thread); + col *= 0.7 + 0.3 * sin(iTime + uv.x * 10.0); + fragColor = vec4(col, 1.0); +}""", ["weave", "textile", "fabric", "pattern", "craft"]), + ("Sierpinski Triangle", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + uv.y = 1.0 - uv.y; + float t = mod(iTime * 0.3, 1.0); + float zoom = pow(2.0, t * 3.0); + uv = uv * zoom - vec2(zoom * 0.5 - 0.5); + float v = 1.0; + for (int i = 0; i < 20; i++) { + if (uv.x + uv.y > 1.0) { v = 0.0; break; } + uv *= 2.0; + if (uv.x > 1.0) uv.x -= 1.0; + if (uv.y > 1.0) uv.y -= 1.0; + } + vec3 col = vec3(v) * vec3(0.3, 0.6, 0.9); + fragColor = vec4(col, 1.0); +}""", ["sierpinski", "triangle", "fractal", "self-similar", "zoom"]), + ("Perlin Contours", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), + mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float n = noise(uv * 6.0 + iTime * 0.2) * 0.5 + + noise(uv * 12.0 - iTime * 0.15) * 0.25 + + noise(uv * 24.0 + iTime * 0.1) * 0.125; + float contour = abs(fract(n * 10.0) - 0.5) * 2.0; + contour = smoothstep(0.0, 0.1, contour); + vec3 col = mix(vec3(0.9, 0.85, 0.75), vec3(0.2, 0.15, 0.1), 1.0 - contour); + fragColor = vec4(col, 1.0); +}""", ["contour", "topographic", "map", "noise", "minimal", "clean"]), + ("Stained Glass", """ +vec2 hash2(vec2 p) { + p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3))); + return fract(sin(p) * 43758.5453); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.y; + vec2 n = floor(uv * 5.0); + vec2 f = fract(uv * 5.0); + float md = 8.0; + vec2 id; + for (int j = -1; j <= 1; j++) + for (int i = -1; i <= 1; i++) { + vec2 g = vec2(float(i), float(j)); + vec2 o = hash2(n + g); + o = 0.5 + 0.45 * sin(iTime * 0.5 + 6.283 * o); + vec2 r = g + o - f; + float d = dot(r, r); + if (d < md) { md = d; id = n + g; } + } + float h = fract(sin(dot(id, vec2(127.1, 311.7))) * 43758.5453); + vec3 col = 0.5 + 0.5 * cos(h * 6.283 + vec3(0, 2, 4)); + col *= 0.7 + 0.3 * smoothstep(0.0, 0.05, md); + float edge = smoothstep(0.01, 0.02, md); + col *= 0.2 + 0.8 * edge; + fragColor = vec4(col, 1.0); +}""", ["stained-glass", "voronoi", "colorful", "mosaic", "church", "art"]), + ("Zen Sand Garden", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float r = length(uv); + float a = atan(uv.y, uv.x); + float rake = sin(r * 40.0 + a * 0.5 - iTime * 0.3); + float stone = smoothstep(0.15, 0.14, length(uv - vec2(0.3, 0.1))); + stone += smoothstep(0.08, 0.07, length(uv + vec2(0.2, 0.15))); + rake *= (1.0 - stone); + vec3 sand = vec3(0.85, 0.78, 0.65); + vec3 col = sand + vec3(0.05) * rake; + col = mix(col, vec3(0.3, 0.3, 0.28), stone); + fragColor = vec4(col, 1.0); +}""", ["zen", "sand", "garden", "minimal", "japanese", "calm", "meditation"]), + ("Wave Interference 2D", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float v = 0.0; + vec2 sources[4]; + sources[0] = vec2(sin(iTime), cos(iTime)) * 0.4; + sources[1] = vec2(-sin(iTime * 0.7), sin(iTime * 0.5)) * 0.5; + sources[2] = vec2(cos(iTime * 0.3), -sin(iTime * 0.8)) * 0.3; + sources[3] = vec2(-cos(iTime * 0.6), cos(iTime * 0.4)) * 0.35; + for (int i = 0; i < 4; i++) { + float d = length(uv - sources[i]); + v += sin(d * 30.0 - iTime * 5.0) / (1.0 + d * 5.0); + } + vec3 col = 0.5 + 0.5 * cos(v * 3.0 + vec3(0, 2, 4)); + fragColor = vec4(col, 1.0); +}""", ["wave", "interference", "physics", "ripple", "colorful", "multi-source"]), + ("DNA Helix", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + uv.y = uv.y * 2.0 - 1.0; + float t = iTime * 2.0; + float x = uv.x * 10.0 + t; + float strand1 = sin(x) * 0.3; + float strand2 = -sin(x) * 0.3; + float d1 = abs(uv.y - strand1); + float d2 = abs(uv.y - strand2); + float line1 = 0.005 / (d1 + 0.005); + float line2 = 0.005 / (d2 + 0.005); + float rung = 0.0; + float rungX = fract(x / 3.14159 * 0.5); + if (rungX < 0.05 || rungX > 0.95) { + float dy = uv.y; + if (dy > min(strand1, strand2) && dy < max(strand1, strand2)) + rung = 0.5; + } + vec3 col = vec3(0.02, 0.0, 0.05); + col += vec3(0.0, 0.6, 1.0) * line1; + col += vec3(1.0, 0.3, 0.5) * line2; + col += vec3(0.3, 0.8, 0.4) * rung; + fragColor = vec4(col, 1.0); +}""", ["dna", "helix", "biology", "science", "double-helix", "blue-pink"]), + ("Pendulum Wave", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + vec3 col = vec3(0.02, 0.01, 0.04); + for (int i = 0; i < 20; i++) { + float fi = float(i); + float freq = 10.0 + fi * 0.5; + float phase = sin(iTime * freq * 0.1) * 0.3; + float x = (fi + 0.5) / 20.0; + float y = 0.5 + phase; + float d = length(vec2(uv.x - x, uv.y - y) * vec2(1.0, 2.0)); + col += vec3(0.5 + 0.5 * cos(fi * 0.3 + vec3(0, 2, 4))) * 0.005 / (d + 0.005); + float rod = smoothstep(0.003, 0.0, abs(uv.x - x)) * step(y, uv.y) * step(uv.y, 0.95); + col += vec3(0.15) * rod; + } + fragColor = vec4(col, 1.0); +}""", ["pendulum", "wave", "physics", "harmonic", "colorful", "simulation"]), + ("ASCII Art Rain", """ +float char(int n, vec2 p) { + p = floor(p * vec2(4.0, -4.0) + 2.5); + if (clamp(p.x, 0.0, 4.0) == p.x && clamp(p.y, 0.0, 4.0) == p.y) { + int bit = int(p.x) + int(p.y) * 5; + if (((n >> bit) & 1) == 1) return 1.0; + } + return 0.0; +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + vec2 grid = floor(uv * vec2(40.0, 25.0)); + float t = floor(iTime * 10.0); + float drop = fract(sin(dot(grid.x + t * 0.01, 78.233)) * 43758.5) * 25.0; + float fade = smoothstep(drop, drop - 8.0, grid.y); + float bright = step(abs(grid.y - drop), 0.5); + vec3 col = vec3(0.0, 0.4, 0.15) * fade + vec3(0.0, 0.9, 0.4) * bright; + fragColor = vec4(col, 1.0); +}""", ["ascii", "rain", "text", "matrix", "digital", "green", "code"]), + ("Smoke Wisps", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), + mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float t = iTime * 0.3; + vec2 p = uv * 3.0; + p.y += t; + float n = noise(p) * 0.5 + noise(p * 2.0 + t) * 0.25 + noise(p * 4.0 - t * 0.5) * 0.125; + float wisp = pow(n, 3.0) * smoothstep(0.0, 0.5, uv.y) * smoothstep(1.0, 0.5, uv.y); + wisp *= smoothstep(0.0, 0.3, uv.x) * smoothstep(1.0, 0.7, uv.x); + vec3 col = vec3(wisp * 0.6); + col += vec3(0.02, 0.01, 0.03); + fragColor = vec4(col, 1.0); +}""", ["smoke", "wisps", "atmospheric", "grey", "subtle", "elegant", "minimal"]), +] + +for name, code, extra_tags in patterns: + s(name, code, + ["pattern", "2d"] + extra_tags, + "2d", f"{name} — procedural pattern", + {"chaos_level": 0.3, "color_temperature": "neutral", "motion_type": "animated"}) + +# --- Family: Domain warping (12 shaders) --- +warp_names = [ + ("Acid Trip", 0.9, "vec3(1.0,0.2,0.8)", "vec3(0.1,0.9,0.3)", ["acid", "psychedelic", "trippy", "wild"]), + ("Deep Dream", 0.7, "vec3(0.3,0.1,0.6)", "vec3(0.8,0.6,0.2)", ["dream", "deep", "surreal", "abstract"]), + ("Oil Slick", 0.4, "vec3(0.2,0.5,0.8)", "vec3(0.8,0.3,0.5)", ["oil", "iridescent", "rainbow", "surface"]), + ("Lava Rift", 0.8, "vec3(1.0,0.3,0.0)", "vec3(0.1,0.0,0.0)", ["lava", "rift", "crack", "hot"]), + ("Ghost Veil", 0.3, "vec3(0.7,0.7,0.8)", "vec3(0.1,0.1,0.15)", ["ghost", "veil", "ethereal", "pale"]), + ("Coral Growth", 0.5, "vec3(0.9,0.4,0.5)", "vec3(0.1,0.3,0.4)", ["coral", "growth", "organic", "living"]), + ("Storm Eye", 0.85, "vec3(0.3,0.4,0.6)", "vec3(0.05,0.05,0.1)", ["storm", "eye", "cyclone", "dramatic"]), + ("Crystal Melt", 0.6, "vec3(0.6,0.8,1.0)", "vec3(0.2,0.1,0.3)", ["crystal", "melt", "dissolve", "transformation"]), + ("Plasma Web", 0.75, "vec3(0.0,1.0,0.8)", "vec3(0.3,0.0,0.5)", ["plasma", "web", "interconnected", "energy"]), + ("Sand Ripple", 0.2, "vec3(0.9,0.8,0.6)", "vec3(0.6,0.5,0.3)", ["sand", "ripple", "desert", "zen"]), + ("Cosmic Dust", 0.65, "vec3(0.5,0.4,0.8)", "vec3(0.05,0.02,0.1)", ["cosmic", "dust", "space", "purple"]), + ("Magma Pool", 0.9, "vec3(1.0,0.5,0.0)", "vec3(0.2,0.0,0.0)", ["magma", "pool", "volcanic", "orange"]), +] + +for i, (name, chaos, c1, c2, extra_tags) in enumerate(warp_names): + warp_freq = 2.0 + (i % 5) + warp_amp = 0.5 + chaos * 0.5 + code = f"""float hash(vec2 p) {{ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }} +float noise(vec2 p) {{ + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), + mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y); +}} +float fbm(vec2 p) {{ + float v = 0.0, a = 0.5; + for (int i = 0; i < 6; i++) {{ v += a * noise(p); p *= 2.0; a *= 0.5; }} + return v; +}} +void mainImage(out vec4 fragColor, in vec2 fragCoord) {{ + vec2 uv = fragCoord / iResolution.xy; + float t = iTime * 0.2; + vec2 p = uv * {warp_freq:.1f}; + float warp1 = fbm(p + t); + float warp2 = fbm(p + warp1 * {warp_amp:.1f} + t * 0.7); + float warp3 = fbm(p + warp2 * {warp_amp:.1f} - t * 0.3); + vec3 col = mix({c2}, {c1}, warp3); + col = pow(col, vec3(0.9)); + fragColor = vec4(col, 1.0); +}}""" + s(name, code, + ["domain-warp", "fbm", "fluid", "animated"] + extra_tags, + "2d", f"Triple-layer domain warping: {name.lower()}", + {"chaos_level": chaos, "color_temperature": ["warm", "cool", "neutral"][i % 3], "motion_type": "fluid"}) + +# --- Family: Raymarched scenes (10 shaders) --- +scenes_3d = [ + ("Floating Islands", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i+vec2(1,0)), f.x), mix(hash(i+vec2(0,1)), hash(i+vec2(1,1)), f.x), f.y); +} +float map(vec3 p) { + float ground = p.y + 1.0; + float island1 = length(p - vec3(0, sin(iTime)*0.3, 0)) - 1.0; + float island2 = length(p - vec3(2.5, cos(iTime*0.7)*0.4+0.5, 1.0)) - 0.6; + float island3 = length(p - vec3(-2.0, sin(iTime*0.5)*0.2+0.3, -0.5)) - 0.8; + return min(min(island1, island2), island3); +} +vec3 getNormal(vec3 p) { + vec2 e = vec2(0.001, 0); + return normalize(vec3(map(p+e.xyy)-map(p-e.xyy),map(p+e.yxy)-map(p-e.yxy),map(p+e.yyx)-map(p-e.yyx))); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec3 ro = vec3(0, 0, -5); + vec3 rd = normalize(vec3(uv, 1.0)); + float t = 0.0; + vec3 col = mix(vec3(0.3, 0.5, 0.9), vec3(0.7, 0.8, 1.0), uv.y + 0.5); + for (int i = 0; i < 80; i++) { + float d = map(ro + rd * t); + if (d < 0.001) { + vec3 p = ro + rd * t; + vec3 n = getNormal(p); + float sun = max(dot(n, normalize(vec3(1, 0.8, -0.5))), 0.0); + col = vec3(0.3, 0.6, 0.2) * (0.3 + 0.7 * sun); + col = mix(col, vec3(0.5, 0.7, 1.0), 1.0 - exp(-t * 0.05)); + break; + } + t += d; + if (t > 30.0) break; + } + fragColor = vec4(col, 1.0); +}""", ["floating", "islands", "3d", "fantasy", "sky", "peaceful", "raymarching"]), + ("Endless Staircase", """ +float map(vec3 p) { + p.y += iTime * 0.5; + vec3 q = mod(p + 1.0, 2.0) - 1.0; + float box = max(max(abs(q.x) - 0.8, abs(q.y) - 0.1), abs(q.z) - 0.4); + float step1 = max(max(abs(q.x) - 0.3, abs(q.y + 0.3) - 0.1), abs(q.z - 0.2) - 0.4); + return min(box, step1); +} +vec3 getNormal(vec3 p) { + vec2 e = vec2(0.001, 0); + return normalize(vec3(map(p+e.xyy)-map(p-e.xyy),map(p+e.yxy)-map(p-e.yxy),map(p+e.yyx)-map(p-e.yyx))); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec3 ro = vec3(0, 0, -4); + vec3 rd = normalize(vec3(uv, 1.0)); + float a = iTime * 0.2; + rd.xz = mat2(cos(a), sin(a), -sin(a), cos(a)) * rd.xz; + float t = 0.0; + vec3 col = vec3(0.05); + for (int i = 0; i < 80; i++) { + float d = map(ro + rd * t); + if (d < 0.001) { + vec3 p = ro + rd * t; + vec3 n = getNormal(p); + float light = max(dot(n, normalize(vec3(1, 1, -1))), 0.0); + col = vec3(0.7, 0.7, 0.75) * (0.2 + 0.8 * light); + col = mix(col, vec3(0.05), 1.0 - exp(-t * 0.08)); + break; + } + t += d; + if (t > 20.0) break; + } + fragColor = vec4(col, 1.0); +}""", ["staircase", "escher", "infinite", "3d", "architectural", "surreal", "impossible"]), + ("Crystal Cave", """ +float map(vec3 p) { + float cave = -(length(p.xz) - 3.0); + float crystal = 1e10; + for (int i = 0; i < 6; i++) { + float fi = float(i); + vec3 cp = vec3(sin(fi*1.0)*2.0, fi*0.5-1.0, cos(fi*1.0)*2.0); + float h = max(abs(p.x-cp.x)+abs(p.z-cp.z)-0.15, abs(p.y-cp.y)-0.4-fi*0.1); + crystal = min(crystal, h); + } + return max(cave, -crystal); +} +vec3 getNormal(vec3 p) { + vec2 e = vec2(0.001, 0); + return normalize(vec3(map(p+e.xyy)-map(p-e.xyy),map(p+e.yxy)-map(p-e.yxy),map(p+e.yyx)-map(p-e.yyx))); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float a = iTime * 0.3; + vec3 ro = vec3(sin(a)*2.0, 0.0, cos(a)*2.0); + vec3 ta = vec3(0.0, 0.0, 0.0); + vec3 fwd = normalize(ta - ro); + vec3 right = normalize(cross(vec3(0,1,0), fwd)); + vec3 up = cross(fwd, right); + vec3 rd = normalize(uv.x * right + uv.y * up + 1.5 * fwd); + float t = 0.0; + vec3 col = vec3(0.01, 0.0, 0.02); + for (int i = 0; i < 80; i++) { + float d = map(ro + rd * t); + if (d < 0.001) { + vec3 p = ro + rd * t; + vec3 n = getNormal(p); + float light = max(dot(n, normalize(vec3(0, 1, 0))), 0.0); + float fresnel = pow(1.0 - abs(dot(n, rd)), 3.0); + col = vec3(0.2, 0.5, 0.8) * light + vec3(0.5, 0.3, 0.9) * fresnel; + break; + } + t += d; + if (t > 20.0) break; + } + fragColor = vec4(col, 1.0); +}""", ["crystal", "cave", "3d", "underground", "fantasy", "blue", "raymarching"]), +] + +for name, code, extra_tags in scenes_3d: + s(name, code, extra_tags, "3d", f"Raymarched 3D scene: {name}", + {"chaos_level": 0.4, "color_temperature": "cool", "motion_type": "orbiting"}) + +# --- Family: Glitch/distortion effects (10 shaders) --- +glitch_variants = [ + ("VHS Tracking", 0.01, 8.0, ["vhs", "tracking", "analog", "retro", "distortion"]), + ("CRT Phosphor", 0.005, 3.0, ["crt", "phosphor", "monitor", "vintage", "scanline"]), + ("Databend", 0.02, 15.0, ["databend", "corruption", "digital-art", "experimental"]), + ("Signal Loss", 0.015, 6.0, ["signal", "loss", "static", "broadcast", "error"]), + ("Pixel Sort", 0.008, 10.0, ["pixel-sort", "glitch-art", "sorting", "avant-garde"]), + ("Bitcrush", 0.025, 4.0, ["bitcrush", "low-res", "lo-fi", "8bit", "quantize"]), + ("Chroma Aberration", 0.012, 5.0, ["chroma", "aberration", "lens", "rgb", "optics"]), + ("Digital Decay", 0.018, 12.0, ["digital-decay", "entropy", "corruption", "time"]), + ("Broadcast Storm", 0.03, 7.0, ["broadcast", "storm", "interference", "signal"]), + ("Codec Artifact", 0.02, 9.0, ["codec", "artifact", "compression", "jpeg", "macro-block"]), +] + +for i, (name, intensity, freq, extra_tags) in enumerate(glitch_variants): + color_shift = ["vec3(0.0, 0.8, 0.3)", "vec3(0.8, 0.2, 0.5)", "vec3(0.2, 0.5, 1.0)", + "vec3(1.0, 0.6, 0.0)", "vec3(0.6, 0.0, 0.9)"][i % 5] + code = f"""float hash(float n) {{ return fract(sin(n) * 43758.5453); }} +void mainImage(out vec4 fragColor, in vec2 fragCoord) {{ + vec2 uv = fragCoord / iResolution.xy; + float t = floor(iTime * {freq:.1f}); + float glitch = step(0.8, hash(t + uv.y * 10.0)); + float blockY = floor(uv.y * 15.0); + float shift = hash(blockY + t) * {intensity * 20:.2f} * glitch; + uv.x = fract(uv.x + shift); + float scanline = sin(uv.y * 400.0 + iTime * 30.0) * 0.03; + float noise = hash(uv.y * 100.0 + t) * glitch * 0.2; + vec3 col; + col.r = 0.5 + 0.5 * sin(uv.x * 6.283 * 3.0 + iTime + {float(i):.1f}); + col.g = 0.5 + 0.5 * sin(uv.x * 6.283 * 3.0 + iTime * 1.1 + 2.0 + {float(i):.1f}); + col.b = 0.5 + 0.5 * sin(uv.x * 6.283 * 3.0 + iTime * 0.9 + 4.0 + {float(i):.1f}); + col += scanline + noise; + col *= 0.7 + 0.3 * (1.0 - glitch); + col += {color_shift} * glitch * 0.3; + fragColor = vec4(col, 1.0); +}}""" + s(name, code, + ["glitch", "distortion", "digital", "animated"] + extra_tags, + "2d", f"Glitch effect: {name.lower()}", + {"chaos_level": 0.7 + (i % 3) * 0.1, "color_temperature": "neutral", "motion_type": "chaotic"}) + +# --- Family: Minimalist geometric (10 shaders) --- +minimals = [ + ("Single Circle", "smoothstep(0.302, 0.3, length(uv))", "vec3(0.95)", "vec3(0.05)", ["circle", "minimal", "clean"]), + ("Double Ring", "smoothstep(0.01,0.0,abs(length(uv)-0.3))+smoothstep(0.01,0.0,abs(length(uv)-0.5))", "vec3(0.9)", "vec3(0.05)", ["rings", "double", "minimal"]), + ("Moving Dot", "smoothstep(0.06,0.05,length(uv-vec2(sin(iTime)*0.3,cos(iTime*0.7)*0.3)))", "vec3(1.0,0.3,0.2)", "vec3(0.02)", ["dot", "motion", "minimal"]), + ("Cross Hair", "smoothstep(0.005,0.0,min(abs(uv.x),abs(uv.y)))*smoothstep(0.5,0.0,length(uv))", "vec3(0.9,0.1,0.1)", "vec3(0.02)", ["crosshair", "target", "minimal"]), + ("Breathing Square", "step(max(abs(uv.x),abs(uv.y)),0.2+sin(iTime)*0.1)", "vec3(0.9)", "vec3(0.05)", ["square", "breathing", "minimal"]), + ("Line Scan", "smoothstep(0.005,0.0,abs(uv.x-sin(iTime)*0.5))", "vec3(0.0,0.8,1.0)", "vec3(0.02)", ["line", "scan", "minimal"]), + ("Dot Grid", "smoothstep(0.05,0.04,length(fract(uv*5.0+0.5)-0.5))", "vec3(0.3,0.3,0.35)", "vec3(0.05)", ["grid", "dots", "minimal"]), + ("Gradient Only", "uv.y*0.5+0.5", "vec3(0.1,0.1,0.3)", "vec3(0.0)", ["gradient", "simple", "minimal"]), + ("Orbit", "smoothstep(0.04,0.03,length(uv-0.3*vec2(cos(iTime),sin(iTime))))", "vec3(1.0,0.8,0.2)", "vec3(0.03,0.01,0.05)", ["orbit", "planet", "minimal"]), + ("Pulse Ring", "smoothstep(0.01,0.0,abs(length(uv)-(mod(iTime,2.0)*0.5)))*exp(-mod(iTime,2.0)*2.0)", "vec3(0.0,0.9,0.5)", "vec3(0.02)", ["pulse", "ring", "expanding", "minimal"]), +] + +for i, (name, expr, fg, bg, extra_tags) in enumerate(minimals): + code = f"""void mainImage(out vec4 fragColor, in vec2 fragCoord) {{ + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float v = {expr}; + vec3 col = mix({bg}, {fg}, v); + fragColor = vec4(col, 1.0); +}}""" + s(name, code, + ["minimal", "geometric", "clean", "simple"] + extra_tags, + "2d", f"Minimalist: {name.lower()}", + {"chaos_level": 0.05 + (i % 3) * 0.05, "color_temperature": ["monochrome", "cool", "warm"][i % 3], "motion_type": "static" if i % 3 == 0 else "animated"}) + +# --- Family: More 3D environments (8 shaders) --- +more_3d = [ + ("Infinite Columns", """ +float map(vec3 p) { + vec2 q = mod(p.xz + 1.0, 2.0) - 1.0; + float col = length(q) - 0.2; + float floor_d = p.y + 1.5; + float ceil_d = -(p.y - 3.0); + return min(min(col, floor_d), ceil_d); +} +vec3 getNormal(vec3 p) { + vec2 e = vec2(0.001, 0); + return normalize(vec3(map(p+e.xyy)-map(p-e.xyy),map(p+e.yxy)-map(p-e.yxy),map(p+e.yyx)-map(p-e.yyx))); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec3 ro = vec3(iTime * 0.5, 0.0, iTime * 0.3); + vec3 rd = normalize(vec3(uv, 1.0)); + float t = 0.0; + vec3 col = vec3(0.02); + for (int i = 0; i < 80; i++) { + float d = map(ro + rd * t); + if (d < 0.001) { + vec3 p = ro + rd * t; + vec3 n = getNormal(p); + float light = max(dot(n, normalize(vec3(1, 1, -1))), 0.0); + col = vec3(0.8, 0.75, 0.7) * (0.15 + 0.85 * light); + col = mix(col, vec3(0.02), 1.0 - exp(-t * 0.06)); + break; + } + t += d; + if (t > 30.0) break; + } + fragColor = vec4(col, 1.0); +}""", ["columns", "infinite", "3d", "architectural", "ancient", "temple"]), + ("Wormhole", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float r = length(uv); + float a = atan(uv.y, uv.x); + float tunnel = 1.0 / (r + 0.1); + float twist = a + tunnel * 0.5 + iTime; + float tex = sin(twist * 4.0) * sin(tunnel * 3.0 - iTime * 4.0); + float glow = exp(-r * 2.0); + vec3 col = vec3(0.02, 0.0, 0.05); + col += vec3(0.2, 0.5, 1.0) * tex * glow; + col += vec3(0.8, 0.4, 1.0) * glow * 0.3; + col += vec3(1.0, 0.9, 0.7) * exp(-r * 8.0) * 0.5; + fragColor = vec4(max(col, 0.0), 1.0); +}""", ["wormhole", "tunnel", "space", "3d-illusion", "portal", "sci-fi", "blue"]), + ("Cubic Lattice", """ +float map(vec3 p) { + vec3 q = mod(p + 0.5, 1.0) - 0.5; + float bars = min(min( + max(abs(q.x), abs(q.y)) - 0.08, + max(abs(q.y), abs(q.z)) - 0.08), + max(abs(q.x), abs(q.z)) - 0.08); + return bars; +} +vec3 getNormal(vec3 p) { + vec2 e = vec2(0.001, 0); + return normalize(vec3(map(p+e.xyy)-map(p-e.xyy),map(p+e.yxy)-map(p-e.yxy),map(p+e.yyx)-map(p-e.yyx))); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float t = iTime * 0.3; + vec3 ro = vec3(sin(t)*3.0, 1.0+sin(t*0.7), cos(t)*3.0); + vec3 ta = vec3(0.0); + vec3 fwd = normalize(ta - ro); + vec3 right = normalize(cross(vec3(0,1,0), fwd)); + vec3 up = cross(fwd, right); + vec3 rd = normalize(uv.x*right + uv.y*up + 1.5*fwd); + float d_t = 0.0; + vec3 col = vec3(0.02); + for (int i = 0; i < 80; i++) { + float d = map(ro + rd * d_t); + if (d < 0.001) { + vec3 p = ro + rd * d_t; + vec3 n = getNormal(p); + float light = max(dot(n, normalize(vec3(1,1,-1))), 0.0); + col = vec3(0.9, 0.5, 0.2) * (0.2 + 0.8 * light); + col = mix(col, vec3(0.02), 1.0 - exp(-d_t * 0.1)); + break; + } + d_t += d; + if (d_t > 20.0) break; + } + fragColor = vec4(col, 1.0); +}""", ["lattice", "cubic", "3d", "wireframe", "structural", "orange", "geometric"]), +] + +for name, code, extra_tags in more_3d: + s(name, code, extra_tags, "3d", f"3D scene: {name}", + {"chaos_level": 0.4, "color_temperature": "cool", "motion_type": "forward"}) + +# --- Family: Color theory experiments (10 shaders) --- +color_exps = [ + ("RGB Separation", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float t = iTime; + float r = length(uv - vec2(sin(t)*0.2, cos(t)*0.15)) - 0.3; + float g = length(uv - vec2(sin(t*1.1+2.0)*0.2, cos(t*0.9+1.0)*0.15)) - 0.3; + float b = length(uv - vec2(sin(t*0.8+4.0)*0.2, cos(t*1.2+3.0)*0.15)) - 0.3; + vec3 col; + col.r = smoothstep(0.01, 0.0, r); + col.g = smoothstep(0.01, 0.0, g); + col.b = smoothstep(0.01, 0.0, b); + fragColor = vec4(col, 1.0); +}""", ["rgb", "separation", "additive", "color-theory", "circles", "primary-colors"]), + ("CMY Overlap", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float t = iTime * 0.5; + float c = smoothstep(0.01, 0.0, length(uv - 0.15*vec2(sin(t), cos(t))) - 0.25); + float m = smoothstep(0.01, 0.0, length(uv - 0.15*vec2(sin(t+2.1), cos(t+2.1))) - 0.25); + float y = smoothstep(0.01, 0.0, length(uv - 0.15*vec2(sin(t+4.2), cos(t+4.2))) - 0.25); + vec3 col = vec3(1.0) - vec3(c, 0.0, 0.0) - vec3(0.0, m, 0.0) - vec3(0.0, 0.0, y); + col = max(col, 0.0); + col = mix(vec3(0.95), col, max(max(c, m), y)); + fragColor = vec4(col, 1.0); +}""", ["cmy", "subtractive", "color-theory", "overlap", "pastel", "print"]), + ("Hue Wheel", """ +vec3 hsv(float h, float s, float v) { + vec3 c = clamp(abs(mod(h*6.0+vec3(0,4,2),6.0)-3.0)-1.0, 0.0, 1.0); + return v * mix(vec3(1.0), c, s); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float a = atan(uv.y, uv.x) / 6.2832 + 0.5; + float r = length(uv); + vec3 col = hsv(a + iTime * 0.1, smoothstep(0.1, 0.4, r), smoothstep(0.5, 0.2, r)); + float ring = smoothstep(0.01, 0.0, abs(r - 0.35)); + col += ring * 0.3; + fragColor = vec4(col, 1.0); +}""", ["hue", "wheel", "color-theory", "hsv", "rainbow", "spectrum", "educational"]), +] + +for name, code, extra_tags in color_exps: + s(name, code, extra_tags, "2d", f"Color theory: {name}", + {"chaos_level": 0.2, "color_temperature": "neutral", "motion_type": "animated"}) + +# --- Family: Particle systems (7 shaders) --- +particle_types = [ + ("Fireflies", "vec3(1.0,0.9,0.3)", 0.003, 30, ["fireflies", "night", "gentle", "nature"]), + ("Snow Fall", "vec3(0.9,0.95,1.0)", 0.004, 50, ["snow", "winter", "cold", "peaceful"]), + ("Ember Rise", "vec3(1.0,0.4,0.0)", 0.003, 40, ["ember", "fire", "rising", "warm"]), + ("Dust Motes", "vec3(0.8,0.7,0.5)", 0.005, 25, ["dust", "motes", "sunbeam", "atmospheric"]), + ("Pollen Drift", "vec3(0.9,0.8,0.2)", 0.006, 20, ["pollen", "spring", "floating", "organic"]), + ("Star Rain", "vec3(0.8,0.9,1.0)", 0.002, 60, ["stars", "rain", "cosmic", "shower"]), + ("Bubble Float", "vec3(0.5,0.8,1.0)", 0.008, 15, ["bubbles", "float", "underwater", "playful"]), +] + +for i, (name, color, size, count, extra_tags) in enumerate(particle_types): + code = f"""float hash(float n) {{ return fract(sin(n) * 43758.5453); }} +void mainImage(out vec4 fragColor, in vec2 fragCoord) {{ + vec2 uv = fragCoord / iResolution.xy; + vec3 col = vec3(0.02, 0.01, 0.04); + for (int i = 0; i < {count}; i++) {{ + float fi = float(i); + float seed = fi * 137.0 + {float(i * 17):.1f}; + vec2 pos; + pos.x = hash(seed); + pos.y = fract(hash(seed + 1.0) + iTime * (0.02 + hash(seed + 2.0) * 0.04) * {1.0 if i % 2 == 0 else -1.0:.1f}); + pos.x += sin(iTime * hash(seed + 3.0) + fi) * 0.02; + float d = length(uv - pos); + float brightness = hash(seed + 4.0) * 0.5 + 0.5; + float twinkle = sin(iTime * (2.0 + hash(seed + 5.0) * 3.0) + fi) * 0.3 + 0.7; + col += {color} * ({size} / (d + {size})) * brightness * twinkle; + }} + fragColor = vec4(col, 1.0); +}}""" + s(name, code, + ["particles", "floating", "ambient", "animated"] + extra_tags, + "2d", f"Ambient particle system: {name.lower()}", + {"chaos_level": 0.2 + (i % 3) * 0.1, "color_temperature": ["warm", "cool", "neutral"][i % 3], "motion_type": "floating"}) + +# --- Family: More distinct 3D (10 shaders) --- +distinct_3d = [ + ("Infinite Corridor", """ +float map(vec3 p) { + vec2 q = abs(mod(p.xz, 4.0) - 2.0); + float walls = min(q.x, q.y) - 0.1; + float ceiling = abs(p.y) - 2.0; + return max(-walls, ceiling); +} +vec3 getNormal(vec3 p) { + vec2 e = vec2(0.001, 0); + return normalize(vec3(map(p+e.xyy)-map(p-e.xyy),map(p+e.yxy)-map(p-e.yxy),map(p+e.yyx)-map(p-e.yyx))); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec3 ro = vec3(0, 0, iTime); + vec3 rd = normalize(vec3(uv, 1.0)); + float t = 0.0; + vec3 col = vec3(0.02); + for (int i = 0; i < 80; i++) { + float d = map(ro + rd * t); + if (abs(d) < 0.001) { + vec3 p = ro + rd * t; + vec3 n = getNormal(p); + col = vec3(0.6, 0.7, 0.8) * (0.2 + 0.8 * max(dot(n, vec3(0, 1, 0)), 0.0)); + col *= 1.0 + 0.1 * sin(p.z); + col = mix(col, vec3(0.02), 1.0 - exp(-t * 0.04)); + break; + } + t += abs(d); + if (t > 30.0) break; + } + fragColor = vec4(col, 1.0); +}""", ["corridor", "hallway", "infinite", "3d", "architectural", "perspective"]), + ("Planet Surface", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i+vec2(1,0)), f.x), mix(hash(i+vec2(0,1)), hash(i+vec2(1,1)), f.x), f.y); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float r = length(uv); + float planet_r = 0.4; + vec3 col = vec3(0.0, 0.0, 0.02); + if (r < planet_r) { + float z = sqrt(planet_r * planet_r - r * r); + vec3 n = normalize(vec3(uv, z)); + vec2 texUV = vec2(atan(n.x, n.z) + iTime * 0.2, asin(n.y)); + float land = noise(texUV * 5.0) * 0.5 + noise(texUV * 10.0) * 0.25; + vec3 surface = mix(vec3(0.1, 0.3, 0.8), vec3(0.2, 0.6, 0.15), step(0.45, land)); + float sun = max(dot(n, normalize(vec3(1, 0.5, 0.5))), 0.0); + col = surface * (0.1 + 0.9 * sun); + } + float atmo = smoothstep(planet_r + 0.02, planet_r - 0.02, r) * (1.0 - smoothstep(planet_r - 0.02, planet_r + 0.08, r)); + col += vec3(0.3, 0.5, 1.0) * atmo * 0.3; + col += vec3(0.8, 0.9, 1.0) * 0.001 / (abs(r - planet_r) + 0.005); + fragColor = vec4(col, 1.0); +}""", ["planet", "earth", "space", "3d", "sphere", "globe", "rotating"]), + ("Disco Ball", """ +float map(vec3 p) { return length(p) - 1.0; } +vec3 getNormal(vec3 p) { return normalize(p); } +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + vec3 ro = vec3(0, 0, -3); + vec3 rd = normalize(vec3(uv, 1.0)); + float t = 0.0; + vec3 col = vec3(0.02); + for (int i = 0; i < 40; i++) { + float d = map(ro + rd * t); + if (d < 0.001) { + vec3 p = ro + rd * t; + vec3 n = getNormal(p); + float a1 = atan(n.z, n.x) + iTime * 0.5; + float a2 = acos(n.y); + float grid = step(0.9, fract(a1 * 5.0)) + step(0.9, fract(a2 * 5.0)); + vec3 refl = reflect(rd, n); + float spec = pow(max(dot(refl, normalize(vec3(sin(iTime), 1, cos(iTime)))), 0.0), 32.0); + float mirror = 0.5 + 0.5 * fract(sin(dot(floor(vec2(a1, a2) * 5.0), vec2(127.1, 311.7))) * 43758.5); + col = vec3(0.3) * (1.0 - grid * 0.5) + vec3(1.0) * spec * mirror; + col += vec3(0.8, 0.3, 0.9) * spec * 0.3; + break; + } + t += d; + if (t > 10.0) break; + } + fragColor = vec4(col, 1.0); +}""", ["disco", "ball", "mirror", "3d", "party", "retro", "reflective", "fun"]), +] + +for name, code, extra_tags in distinct_3d: + s(name, code, extra_tags, "3d", f"3D scene: {name}", + {"chaos_level": 0.4, "color_temperature": "cool", "motion_type": "rotating"}) + +# --- Standalone standouts (7 more to break 200) --- + +s("Supernova", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float r = length(uv); + float a = atan(uv.y, uv.x); + float t = iTime; + float shockwave = smoothstep(0.02, 0.0, abs(r - mod(t * 0.5, 2.0))); + float core = exp(-r * 8.0); + float rays = pow(0.5 + 0.5 * sin(a * 12.0 + t * 3.0), 8.0) * exp(-r * 3.0); + vec3 col = vec3(1.0, 0.8, 0.3) * core; + col += vec3(1.0, 0.3, 0.1) * rays; + col += vec3(0.3, 0.5, 1.0) * shockwave; + fragColor = vec4(col, 1.0); +}""", ["supernova", "explosion", "star", "cosmic", "energy", "bright"], "2d", +"Supernova with expanding shockwave and ray corona", +{"chaos_level": 0.8, "color_temperature": "warm", "motion_type": "explosive"}) + +s("Fabric Fold", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float fold = sin(uv.x * 12.0 + iTime * 0.3) * sin(uv.y * 8.0 - iTime * 0.21) * 0.5; + float shadow = fold * 0.5 + 0.5; + vec3 col = vec3(0.6, 0.15, 0.2) * (0.4 + 0.6 * shadow); + col += vec3(0.3, 0.1, 0.1) * pow(max(shadow, 0.0), 4.0); + fragColor = vec4(col, 1.0); +}""", ["fabric", "fold", "cloth", "textile", "red", "silk", "elegant"], "2d", +"Folded silk fabric with dynamic shadow", +{"chaos_level": 0.2, "color_temperature": "warm", "motion_type": "fluid"}) + +s("EKG Heartbeat", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float x = fract(uv.x * 2.0 - iTime * 0.5); + float y = uv.y * 2.0 - 1.0; + float ekg = 0.0; + if (x < 0.15) ekg = (x / 0.15) * 0.3; + else if (x < 0.2) ekg = (0.2 - x) / 0.05 * 0.3; + else if (x < 0.3) ekg = (x - 0.25) * 16.0; + else if (x < 0.35) ekg = (0.35 - x) * 16.0 - 0.2; + float d = abs(y - ekg); + vec3 col = vec3(0.0, 1.0, 0.3) * 0.004 / (d + 0.004); + col += vec3(0.0, 0.3, 0.1) * 0.015 / (d + 0.015); + fragColor = vec4(col, 1.0); +}""", ["ekg", "heartbeat", "medical", "pulse", "green", "monitor", "health"], "2d", +"EKG heartbeat monitor trace", +{"chaos_level": 0.1, "color_temperature": "cool", "motion_type": "scrolling"}) + +s("Rotating Galaxy Arm", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float r = length(uv); + float a = atan(uv.y, uv.x) + iTime * 0.2; + float arm = max(sin(a * 2.0 - r * 8.0) * exp(-r * 1.5), 0.0); + float stars = step(0.997, hash(floor(fragCoord))); + vec3 col = vec3(0.01, 0.0, 0.02) + vec3(0.3, 0.2, 0.5) * arm + vec3(0.8, 0.6, 0.3) * arm * arm * 2.0; + col += vec3(0.8, 0.85, 1.0) * stars * (1.0 - r); + col += vec3(1.0, 0.9, 0.6) * exp(-r * 12.0) * 0.5; + fragColor = vec4(col, 1.0); +}""", ["galaxy", "spiral-arm", "space", "stars", "cosmic", "astronomy"], "2d", +"Spiral galaxy with rotating arms and star field", +{"chaos_level": 0.4, "color_temperature": "warm", "motion_type": "rotating"}) + +s("Underwater Caustics", """ +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p) { + vec2 i = floor(p); vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i+vec2(1,0)), f.x), mix(hash(i+vec2(0,1)), hash(i+vec2(1,1)), f.x), f.y); +} +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + float t = iTime * 0.5; + float c = 0.0; + for (int i = 0; i < 3; i++) { + float fi = float(i); + vec2 p = uv * (4.0 + fi * 2.0) + vec2(t * (0.3 + fi * 0.1), t * 0.2); + c += abs(sin(noise(p) * 6.283)) * (0.5 / (1.0 + fi)); + } + vec3 col = mix(vec3(0.0, 0.15, 0.35), vec3(0.1, 0.6, 0.8), pow(c, 1.5)); + fragColor = vec4(col, 1.0); +}""", ["underwater", "caustics", "water", "ocean", "blue", "ripple", "serene"], "2d", +"Underwater light caustics on the ocean floor", +{"chaos_level": 0.3, "color_temperature": "cool", "motion_type": "fluid"}) + +s("Geometric Rose", """ +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; + float r = length(uv); + float a = atan(uv.y, uv.x); + float rose = cos(a * 4.0 + iTime) * 0.3 + 0.3; + float inner = cos(a * 7.0 - iTime * 1.5) * 0.15 + 0.15; + vec3 col = vec3(0.02, 0.0, 0.04); + col += vec3(0.9, 0.2, 0.4) * 0.003 / (abs(r - rose) + 0.003); + col += vec3(1.0, 0.6, 0.7) * 0.002 / (abs(r - inner) + 0.002); + col += vec3(0.3, 0.05, 0.1) * exp(-r * 3.0); + fragColor = vec4(col, 1.0); +}""", ["rose", "polar", "mathematical", "curve", "pink", "elegant", "botanical"], "2d", +"Polar rose curves with counter-rotating petals", +{"chaos_level": 0.3, "color_temperature": "warm", "motion_type": "rotating"}) + +s("Data Stream", """ +float hash(float n) { return fract(sin(n) * 43758.5453); } +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + vec3 col = vec3(0.02, 0.01, 0.03); + for (int i = 0; i < 30; i++) { + float fi = float(i); + float lane = (fi + 0.5) / 30.0; + float speed = 0.5 + hash(fi * 7.0) * 2.0; + float y = fract(uv.y * 3.0 + iTime * speed + hash(fi * 13.0)); + float on = step(0.3, hash(floor(y * 20.0) + fi * 100.0 + floor(iTime * speed * 3.0))); + float d = abs(uv.x - lane); + col += (0.5 + 0.5 * cos(fi * 0.5 + vec3(0, 2, 4))) * 0.001 / (d + 0.001) * on * y * 0.1; + } + fragColor = vec4(min(col, 1.0), 1.0); +}""", ["data", "stream", "digital", "network", "colorful", "flow", "tech", "cyber"], "2d", +"Multi-lane data streams at different speeds", +{"chaos_level": 0.5, "color_temperature": "cool", "motion_type": "flowing"}) + + +# ═══════════════════════════════════════════════════════════ + +print(f"Total shaders defined: {len(SHADERS)}") + +# --- Database insertion --- +async def seed(): + import asyncpg + import json + import os + + db_url = os.environ.get("DATABASE_URL_SYNC", "postgresql://fracta:devpass@postgres:5432/fractafrag") + # Convert sync URL to asyncpg format + db_url = db_url.replace("postgresql://", "postgresql://").replace("+asyncpg", "") + + conn = await asyncpg.connect(db_url) + + # Check if already seeded + count = await conn.fetchval("SELECT COUNT(*) FROM shaders WHERE is_system = TRUE") + if count > 0: + print(f"Already seeded ({count} system shaders). Skipping.") + await conn.close() + return + + print(f"Seeding {len(SHADERS)} shaders...") + + # Distribute creation times over past 30 days for realistic feed testing + now = datetime.now(timezone.utc) + total = len(SHADERS) + + for i, shader in enumerate(SHADERS): + age_hours = random.uniform(0, 30 * 24) # up to 30 days ago + created_at = now - timedelta(hours=age_hours) + score = random.uniform(0, 10) # random initial score for feed ranking variety + + shader_id = str(uuid.uuid4()) + + await conn.execute(""" + INSERT INTO shaders (id, author_id, title, description, glsl_code, status, is_public, + is_ai_generated, is_system, system_label, tags, shader_type, + style_metadata, render_status, score, current_version, + created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) + """, + uuid.UUID(shader_id), + uuid.UUID(SYSTEM_USER_ID), + shader["title"], + shader["description"], + shader["code"], + "published", + True, + False, # not AI-generated — these are hand-curated + True, # is_system + "fractafrag-curated", + shader["tags"], + shader["shader_type"], + json.dumps(shader["style_metadata"]), + "ready", # render_status — these render client-side + score, + 1, + created_at, + created_at, + ) + + # Also create version 1 for each + await conn.execute(""" + INSERT INTO shader_versions (id, shader_id, version_number, glsl_code, title, description, + tags, style_metadata, change_note, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + """, + uuid.uuid4(), + uuid.UUID(shader_id), + 1, + shader["code"], + shader["title"], + shader["description"], + shader["tags"], + json.dumps(shader["style_metadata"]), + "Initial version — fractafrag curated content", + created_at, + ) + + if (i + 1) % 25 == 0: + print(f" ...{i + 1}/{total}") + + await conn.close() + print(f"Done. {total} shaders seeded as 'fractafrag-curated' system content.") + +if __name__ == "__main__": + asyncio.run(seed()) diff --git a/services/api/app/models/__init__.py b/services/api/app/models/__init__.py index fde2343..23fbe8a 100644 --- a/services/api/app/models/__init__.py +++ b/services/api/app/models/__init__.py @@ -1,12 +1,14 @@ """Models package.""" from app.models.models import ( - User, Shader, Vote, EngagementEvent, Desire, DesireCluster, + SYSTEM_USER_ID, + User, Shader, ShaderVersion, Vote, EngagementEvent, Desire, DesireCluster, BountyTip, CreatorPayout, ApiKey, GenerationLog, Comment, SourceUnlock, CreatorEngagementSnapshot, ) __all__ = [ - "User", "Shader", "Vote", "EngagementEvent", "Desire", "DesireCluster", + "SYSTEM_USER_ID", + "User", "Shader", "ShaderVersion", "Vote", "EngagementEvent", "Desire", "DesireCluster", "BountyTip", "CreatorPayout", "ApiKey", "GenerationLog", "Comment", "SourceUnlock", "CreatorEngagementSnapshot", ] diff --git a/services/api/app/models/models.py b/services/api/app/models/models.py index 5f3b3f0..c0701a8 100644 --- a/services/api/app/models/models.py +++ b/services/api/app/models/models.py @@ -11,6 +11,9 @@ from pgvector.sqlalchemy import Vector from sqlalchemy.orm import relationship from app.database import Base +# System account UUID — the "fractafrag" platform user +SYSTEM_USER_ID = uuid.UUID("00000000-0000-0000-0000-000000000001") + class User(Base): __tablename__ = "users" @@ -21,19 +24,17 @@ class User(Base): password_hash = Column(String, nullable=False) role = Column(String, nullable=False, default="user") trust_tier = Column(String, nullable=False, default="standard") + is_system = Column(Boolean, nullable=False, default=False) stripe_customer_id = Column(String, nullable=True) subscription_tier = Column(String, default="free") ai_credits_remaining = Column(Integer, default=0) taste_vector = Column(Vector(512), nullable=True) - # Creator economy stubs is_verified_creator = Column(Boolean, default=False) verified_creator_at = Column(DateTime(timezone=True), nullable=True) stripe_connect_account_id = Column(String, nullable=True) - # Timestamps created_at = Column(DateTime(timezone=True), default=datetime.utcnow) last_active_at = Column(DateTime(timezone=True), nullable=True) - # Relationships shaders = relationship("Shader", back_populates="author") votes = relationship("Vote", back_populates="user") api_keys = relationship("ApiKey", back_populates="user") @@ -47,9 +48,12 @@ class Shader(Base): title = Column(String, nullable=False) description = Column(Text, nullable=True) glsl_code = Column(Text, nullable=False) + status = Column(String, nullable=False, default="published") # draft, published, archived is_public = Column(Boolean, default=True) is_ai_generated = Column(Boolean, default=False) + is_system = Column(Boolean, default=False) ai_provider = Column(String, nullable=True) + system_label = Column(String, nullable=True) thumbnail_url = Column(String, nullable=True) preview_url = Column(String, nullable=True) render_status = Column(String, default="pending") @@ -58,20 +62,38 @@ class Shader(Base): tags = Column(ARRAY(String), default=list) shader_type = Column(String, default="2d") forked_from = Column(UUID(as_uuid=True), ForeignKey("shaders.id", ondelete="SET NULL"), nullable=True) + current_version = Column(Integer, nullable=False, default=1) view_count = Column(Integer, default=0) score = Column(Float, default=0.0) - # Creator economy stubs access_tier = Column(String, default="open") source_unlock_price_cents = Column(Integer, nullable=True) commercial_license_price_cents = Column(Integer, nullable=True) verified_creator_shader = Column(Boolean, default=False) - # Timestamps created_at = Column(DateTime(timezone=True), default=datetime.utcnow) updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow) - # Relationships author = relationship("User", back_populates="shaders") votes = relationship("Vote", back_populates="shader") + versions = relationship("ShaderVersion", back_populates="shader", order_by="ShaderVersion.version_number.desc()") + + +class ShaderVersion(Base): + __tablename__ = "shader_versions" + __table_args__ = (UniqueConstraint("shader_id", "version_number"),) + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + shader_id = Column(UUID(as_uuid=True), ForeignKey("shaders.id", ondelete="CASCADE"), nullable=False) + version_number = Column(Integer, nullable=False) + glsl_code = Column(Text, nullable=False) + title = Column(String, nullable=False) + description = Column(Text, nullable=True) + tags = Column(ARRAY(String), default=list) + style_metadata = Column(JSONB, nullable=True) + change_note = Column(Text, nullable=True) + thumbnail_url = Column(String, nullable=True) + created_at = Column(DateTime(timezone=True), default=datetime.utcnow) + + shader = relationship("Shader", back_populates="versions") class Vote(Base): @@ -195,7 +217,6 @@ class Comment(Base): created_at = Column(DateTime(timezone=True), default=datetime.utcnow) -# Creator economy stubs (dormant) class SourceUnlock(Base): __tablename__ = "source_unlocks" diff --git a/services/api/app/routers/feed.py b/services/api/app/routers/feed.py index 4f20e9e..5ad010a 100644 --- a/services/api/app/routers/feed.py +++ b/services/api/app/routers/feed.py @@ -11,6 +11,9 @@ from app.middleware.auth import get_optional_user, get_current_user router = APIRouter() +# Common filter for public, published shaders +_FEED_FILTER = [Shader.is_public == True, Shader.status == "published"] + @router.get("", response_model=list[ShaderFeedItem]) async def get_feed( @@ -19,15 +22,9 @@ async def get_feed( db: AsyncSession = Depends(get_db), user: User | None = Depends(get_optional_user), ): - """ - Personalized feed for authenticated users (pgvector taste match). - Trending/new for anonymous users. - """ - # TODO: Implement full recommendation engine (Track F) - # For now: return newest public shaders query = ( select(Shader) - .where(Shader.is_public == True, Shader.render_status == "ready") + .where(*_FEED_FILTER) .order_by(Shader.created_at.desc()) .limit(limit) ) @@ -42,7 +39,7 @@ async def get_trending( ): query = ( select(Shader) - .where(Shader.is_public == True, Shader.render_status == "ready") + .where(*_FEED_FILTER) .order_by(Shader.score.desc()) .limit(limit) ) @@ -57,7 +54,7 @@ async def get_new( ): query = ( select(Shader) - .where(Shader.is_public == True, Shader.render_status == "ready") + .where(*_FEED_FILTER) .order_by(Shader.created_at.desc()) .limit(limit) ) @@ -71,7 +68,6 @@ async def report_dwell( db: AsyncSession = Depends(get_db), user: User | None = Depends(get_optional_user), ): - """Report dwell time signal for recommendation engine.""" from app.models import EngagementEvent event = EngagementEvent( @@ -83,4 +79,3 @@ async def report_dwell( event_metadata={"replayed": body.replayed}, ) db.add(event) - # TODO: Update user taste vector (Track F) diff --git a/services/api/app/routers/shaders.py b/services/api/app/routers/shaders.py index 75d3ea7..ba93624 100644 --- a/services/api/app/routers/shaders.py +++ b/services/api/app/routers/shaders.py @@ -1,4 +1,4 @@ -"""Shaders router — CRUD, submit, fork, search.""" +"""Shaders router — CRUD, versioning, drafts, fork, search.""" from uuid import UUID from datetime import datetime, timezone @@ -7,25 +7,31 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from app.database import get_db -from app.models import User, Shader -from app.schemas import ShaderCreate, ShaderUpdate, ShaderPublic +from app.models import User, Shader, ShaderVersion +from app.schemas import ShaderCreate, ShaderUpdate, ShaderPublic, ShaderVersionPublic from app.middleware.auth import get_current_user, get_optional_user from app.services.glsl_validator import validate_glsl router = APIRouter() +# ── Public list / search ────────────────────────────────── + @router.get("", response_model=list[ShaderPublic]) async def list_shaders( q: str | None = Query(None, description="Search query"), tags: list[str] | None = Query(None, description="Filter by tags"), shader_type: str | None = Query(None, description="Filter by type: 2d, 3d, audio-reactive"), sort: str = Query("trending", description="Sort: trending, new, top"), + is_system: bool | None = Query(None, description="Filter to system/platform shaders"), limit: int = Query(20, ge=1, le=50), offset: int = Query(0, ge=0), db: AsyncSession = Depends(get_db), ): - query = select(Shader).where(Shader.is_public == True, Shader.render_status == "ready") + query = select(Shader).where( + Shader.is_public == True, + Shader.status == "published", + ) if q: query = query.where(Shader.title.ilike(f"%{q}%")) @@ -33,12 +39,14 @@ async def list_shaders( query = query.where(Shader.tags.overlap(tags)) if shader_type: query = query.where(Shader.shader_type == shader_type) + if is_system is not None: + query = query.where(Shader.is_system == is_system) if sort == "new": query = query.order_by(Shader.created_at.desc()) elif sort == "top": query = query.order_by(Shader.score.desc()) - else: # trending + else: query = query.order_by(Shader.score.desc(), Shader.created_at.desc()) query = query.limit(limit).offset(offset) @@ -46,6 +54,27 @@ async def list_shaders( return result.scalars().all() +# ── My shaders (workspace) ─────────────────────────────── + +@router.get("/mine", response_model=list[ShaderPublic]) +async def my_shaders( + status_filter: str | None = Query(None, alias="status", description="draft, published, archived"), + limit: int = Query(50, ge=1, le=100), + offset: int = Query(0, ge=0), + db: AsyncSession = Depends(get_db), + user: User = Depends(get_current_user), +): + """List the authenticated user's shaders — drafts, published, archived.""" + query = select(Shader).where(Shader.author_id == user.id) + if status_filter: + query = query.where(Shader.status == status_filter) + query = query.order_by(Shader.updated_at.desc()).limit(limit).offset(offset) + result = await db.execute(query) + return result.scalars().all() + + +# ── Single shader ───────────────────────────────────────── + @router.get("/{shader_id}", response_model=ShaderPublic) async def get_shader( shader_id: UUID, @@ -57,47 +86,87 @@ async def get_shader( if not shader: raise HTTPException(status_code=404, detail="Shader not found") + # Drafts are only visible to their author + if shader.status == "draft" and (not user or user.id != shader.author_id): + raise HTTPException(status_code=404, detail="Shader not found") if not shader.is_public and (not user or user.id != shader.author_id): raise HTTPException(status_code=404, detail="Shader not found") - # Increment view count shader.view_count += 1 return shader +# ── Version history ─────────────────────────────────────── + +@router.get("/{shader_id}/versions", response_model=list[ShaderVersionPublic]) +async def list_versions( + shader_id: UUID, + db: AsyncSession = Depends(get_db), + user: User | None = Depends(get_optional_user), +): + """Get the version history of a shader.""" + shader = (await db.execute(select(Shader).where(Shader.id == shader_id))).scalar_one_or_none() + if not shader: + raise HTTPException(status_code=404, detail="Shader not found") + if shader.status == "draft" and (not user or user.id != shader.author_id): + raise HTTPException(status_code=404, detail="Shader not found") + + result = await db.execute( + select(ShaderVersion) + .where(ShaderVersion.shader_id == shader_id) + .order_by(ShaderVersion.version_number.desc()) + ) + return result.scalars().all() + + +@router.get("/{shader_id}/versions/{version_number}", response_model=ShaderVersionPublic) +async def get_version( + shader_id: UUID, + version_number: int, + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(ShaderVersion).where( + ShaderVersion.shader_id == shader_id, + ShaderVersion.version_number == version_number, + ) + ) + version = result.scalar_one_or_none() + if not version: + raise HTTPException(status_code=404, detail="Version not found") + return version + + +# ── Create shader (draft or published) ─────────────────── + @router.post("", response_model=ShaderPublic, status_code=status.HTTP_201_CREATED) async def create_shader( body: ShaderCreate, db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user), ): - # Rate limit: free tier gets 5 submissions/month - if user.subscription_tier == "free": + # Rate limit published shaders for free tier (drafts are unlimited) + if body.status == "published" and user.subscription_tier == "free": month_start = datetime.now(timezone.utc).replace(day=1, hour=0, minute=0, second=0, microsecond=0) count_result = await db.execute( select(func.count()).select_from(Shader).where( Shader.author_id == user.id, + Shader.status == "published", Shader.created_at >= month_start, ) ) monthly_count = count_result.scalar() if monthly_count >= 5: - raise HTTPException( - status_code=429, - detail="Free tier: 5 shader submissions per month. Upgrade to Pro for unlimited." - ) + raise HTTPException(status_code=429, detail="Free tier: 5 published shaders/month. Upgrade to Pro for unlimited.") # Validate GLSL validation = validate_glsl(body.glsl_code, body.shader_type) if not validation.valid: - raise HTTPException( - status_code=422, - detail={ - "message": "GLSL validation failed", - "errors": validation.errors, - "warnings": validation.warnings, - } - ) + raise HTTPException(status_code=422, detail={ + "message": "GLSL validation failed", + "errors": validation.errors, + "warnings": validation.warnings, + }) shader = Shader( author_id=user.id, @@ -106,23 +175,37 @@ async def create_shader( glsl_code=body.glsl_code, tags=body.tags, shader_type=body.shader_type, - is_public=body.is_public, + is_public=body.is_public if body.status == "published" else False, + status=body.status, style_metadata=body.style_metadata, - render_status="pending", + render_status="ready" if body.status == "draft" else "pending", + current_version=1, ) db.add(shader) await db.flush() - # Enqueue render job - from app.worker import celery_app - try: - celery_app.send_task("render_shader", args=[str(shader.id)]) - except Exception: - # If Celery isn't available (dev without worker), mark as ready - # with no thumbnail — the frontend can still render live - shader.render_status = "ready" + # Create version 1 snapshot + v1 = ShaderVersion( + shader_id=shader.id, + version_number=1, + glsl_code=body.glsl_code, + title=body.title, + description=body.description, + tags=body.tags, + style_metadata=body.style_metadata, + change_note="Initial version", + ) + db.add(v1) - # If this shader fulfills a desire, link them + # Enqueue render for published shaders + if body.status == "published": + from app.worker import celery_app + try: + celery_app.send_task("render_shader", args=[str(shader.id)]) + except Exception: + shader.render_status = "ready" + + # Link to desire if fulfilling if body.fulfills_desire_id: from app.models import Desire desire = (await db.execute( @@ -136,6 +219,8 @@ async def create_shader( return shader +# ── Update shader (creates new version) ────────────────── + @router.put("/{shader_id}", response_model=ShaderPublic) async def update_shader( shader_id: UUID, @@ -151,20 +236,40 @@ async def update_shader( raise HTTPException(status_code=403, detail="Not the shader owner") updates = body.model_dump(exclude_unset=True) + change_note = updates.pop("change_note", None) + code_changed = "glsl_code" in updates # Re-validate GLSL if code changed - if "glsl_code" in updates: + if code_changed: validation = validate_glsl(updates["glsl_code"], shader.shader_type) if not validation.valid: - raise HTTPException( - status_code=422, - detail={ - "message": "GLSL validation failed", - "errors": validation.errors, - "warnings": validation.warnings, - } - ) - # Re-render if code changed + raise HTTPException(status_code=422, detail={ + "message": "GLSL validation failed", + "errors": validation.errors, + "warnings": validation.warnings, + }) + + # Apply updates + for field, value in updates.items(): + setattr(shader, field, value) + + # Create a new version snapshot if code or metadata changed + if code_changed or "title" in updates or "description" in updates or "tags" in updates: + shader.current_version += 1 + new_version = ShaderVersion( + shader_id=shader.id, + version_number=shader.current_version, + glsl_code=shader.glsl_code, + title=shader.title, + description=shader.description, + tags=shader.tags, + style_metadata=shader.style_metadata, + change_note=change_note, + ) + db.add(new_version) + + # Re-render if code changed and shader is published + if code_changed and shader.status == "published": shader.render_status = "pending" from app.worker import celery_app try: @@ -172,13 +277,22 @@ async def update_shader( except Exception: shader.render_status = "ready" - for field, value in updates.items(): - setattr(shader, field, value) + # If publishing a draft, ensure it's public and queue render + if "status" in updates and updates["status"] == "published" and shader.render_status != "ready": + shader.is_public = True + shader.render_status = "pending" + from app.worker import celery_app + try: + celery_app.send_task("render_shader", args=[str(shader.id)]) + except Exception: + shader.render_status = "ready" shader.updated_at = datetime.now(timezone.utc) return shader +# ── Delete ──────────────────────────────────────────────── + @router.delete("/{shader_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_shader( shader_id: UUID, @@ -191,10 +305,11 @@ async def delete_shader( raise HTTPException(status_code=404, detail="Shader not found") if shader.author_id != user.id and user.role != "admin": raise HTTPException(status_code=403, detail="Not the shader owner") - await db.delete(shader) +# ── Fork ────────────────────────────────────────────────── + @router.post("/{shader_id}/fork", response_model=ShaderPublic, status_code=status.HTTP_201_CREATED) async def fork_shader( shader_id: UUID, @@ -205,7 +320,7 @@ async def fork_shader( original = result.scalar_one_or_none() if not original: raise HTTPException(status_code=404, detail="Shader not found") - if not original.is_public: + if not original.is_public and original.status != "published": raise HTTPException(status_code=404, detail="Shader not found") forked = Shader( @@ -217,16 +332,83 @@ async def fork_shader( shader_type=original.shader_type, forked_from=original.id, style_metadata=original.style_metadata, - render_status="pending", + status="draft", # Forks start as drafts + is_public=False, + render_status="ready", + current_version=1, ) db.add(forked) await db.flush() - # Enqueue render for the fork - from app.worker import celery_app - try: - celery_app.send_task("render_shader", args=[str(forked.id)]) - except Exception: - forked.render_status = "ready" + v1 = ShaderVersion( + shader_id=forked.id, + version_number=1, + glsl_code=original.glsl_code, + title=forked.title, + description=forked.description, + tags=original.tags, + style_metadata=original.style_metadata, + change_note=f"Forked from {original.title}", + ) + db.add(v1) return forked + + +# ── Restore a version ──────────────────────────────────── + +@router.post("/{shader_id}/versions/{version_number}/restore", response_model=ShaderPublic) +async def restore_version( + shader_id: UUID, + version_number: int, + db: AsyncSession = Depends(get_db), + user: User = Depends(get_current_user), +): + """Restore a shader to a previous version (creates a new version snapshot).""" + shader = (await db.execute(select(Shader).where(Shader.id == shader_id))).scalar_one_or_none() + if not shader: + raise HTTPException(status_code=404, detail="Shader not found") + if shader.author_id != user.id and user.role != "admin": + raise HTTPException(status_code=403, detail="Not the shader owner") + + version = (await db.execute( + select(ShaderVersion).where( + ShaderVersion.shader_id == shader_id, + ShaderVersion.version_number == version_number, + ) + )).scalar_one_or_none() + if not version: + raise HTTPException(status_code=404, detail="Version not found") + + # Apply version data to shader + shader.glsl_code = version.glsl_code + shader.title = version.title + shader.description = version.description + shader.tags = version.tags + shader.style_metadata = version.style_metadata + shader.current_version += 1 + shader.updated_at = datetime.now(timezone.utc) + + # Create a new version snapshot for the restore + restore_v = ShaderVersion( + shader_id=shader.id, + version_number=shader.current_version, + glsl_code=version.glsl_code, + title=version.title, + description=version.description, + tags=version.tags, + style_metadata=version.style_metadata, + change_note=f"Restored from version {version_number}", + ) + db.add(restore_v) + + # Re-render if published + if shader.status == "published": + shader.render_status = "pending" + from app.worker import celery_app + try: + celery_app.send_task("render_shader", args=[str(shader.id)]) + except Exception: + shader.render_status = "ready" + + return shader diff --git a/services/api/app/schemas/schemas.py b/services/api/app/schemas/schemas.py index e2de2bb..7a66ee9 100644 --- a/services/api/app/schemas/schemas.py +++ b/services/api/app/schemas/schemas.py @@ -35,6 +35,7 @@ class UserPublic(BaseModel): id: UUID username: str role: str + is_system: bool subscription_tier: str is_verified_creator: bool created_at: datetime @@ -53,7 +54,6 @@ class UserUpdate(BaseModel): class ByokKeysUpdate(BaseModel): - """Bring Your Own Key — encrypted API keys for AI providers.""" anthropic_key: Optional[str] = Field(None, description="Anthropic API key") openai_key: Optional[str] = Field(None, description="OpenAI API key") ollama_endpoint: Optional[str] = Field(None, description="Ollama endpoint URL") @@ -70,6 +70,7 @@ class ShaderCreate(BaseModel): tags: list[str] = Field(default_factory=list, max_length=10) shader_type: str = Field(default="2d", pattern=r"^(2d|3d|audio-reactive)$") is_public: bool = True + status: str = Field(default="published", pattern=r"^(draft|published)$") style_metadata: Optional[dict] = None fulfills_desire_id: Optional[UUID] = None @@ -80,6 +81,8 @@ class ShaderUpdate(BaseModel): glsl_code: Optional[str] = Field(None, min_length=10) tags: Optional[list[str]] = None is_public: Optional[bool] = None + status: Optional[str] = Field(None, pattern=r"^(draft|published|archived)$") + change_note: Optional[str] = Field(None, max_length=200) class ShaderPublic(BaseModel): @@ -90,9 +93,12 @@ class ShaderPublic(BaseModel): title: str description: Optional[str] glsl_code: str + status: str is_public: bool is_ai_generated: bool + is_system: bool ai_provider: Optional[str] + system_label: Optional[str] thumbnail_url: Optional[str] preview_url: Optional[str] render_status: str @@ -100,6 +106,7 @@ class ShaderPublic(BaseModel): tags: list[str] shader_type: str forked_from: Optional[UUID] + current_version: int view_count: int score: float created_at: datetime @@ -121,10 +128,28 @@ class ShaderFeedItem(BaseModel): score: float view_count: int is_ai_generated: bool + is_system: bool + system_label: Optional[str] style_metadata: Optional[dict] created_at: datetime +class ShaderVersionPublic(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: UUID + shader_id: UUID + version_number: int + glsl_code: str + title: str + description: Optional[str] + tags: list[str] + style_metadata: Optional[dict] + change_note: Optional[str] + thumbnail_url: Optional[str] + created_at: datetime + + # ════════════════════════════════════════════════════════════ # VOTES & ENGAGEMENT # ════════════════════════════════════════════════════════════ @@ -170,13 +195,13 @@ class DesirePublic(BaseModel): class GenerateRequest(BaseModel): prompt: str = Field(..., min_length=5, max_length=500) - provider: Optional[str] = None # anthropic, openai, ollama — auto-selected if None + provider: Optional[str] = None style_metadata: Optional[dict] = None class GenerateStatusResponse(BaseModel): job_id: str - status: str # queued, generating, rendering, complete, failed + status: str shader_id: Optional[UUID] = None error: Optional[str] = None @@ -202,7 +227,6 @@ class ApiKeyPublic(BaseModel): class ApiKeyCreated(ApiKeyPublic): - """Returned only on creation — includes the full key (shown once).""" full_key: str diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index 83871a7..e48b3d3 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -4,6 +4,7 @@ import Feed from './pages/Feed'; import Explore from './pages/Explore'; import ShaderDetail from './pages/ShaderDetail'; import Editor from './pages/Editor'; +import MyShaders from './pages/MyShaders'; import Generate from './pages/Generate'; import Bounties from './pages/Bounties'; import BountyDetail from './pages/BountyDetail'; @@ -21,6 +22,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/services/frontend/src/components/Navbar.tsx b/services/frontend/src/components/Navbar.tsx index 9d3c105..014acdc 100644 --- a/services/frontend/src/components/Navbar.tsx +++ b/services/frontend/src/components/Navbar.tsx @@ -41,6 +41,7 @@ export default function Navbar() {
{isAuthenticated() && user ? ( <> + My Shaders (null); + const [editingExisting, setEditingExisting] = useState(false); + + // Resizable pane state + const [editorWidth, setEditorWidth] = useState(50); // percentage + const isDragging = useRef(false); + const containerRef = useRef(null); const debounceRef = useRef>(); - // Load existing shader for forking + // Load existing shader for editing or forking const { data: existingShader } = useQuery({ queryKey: ['shader', id], queryFn: async () => { @@ -64,22 +74,61 @@ export default function Editor() { if (existingShader) { setCode(existingShader.glsl_code); setLiveCode(existingShader.glsl_code); - setTitle(`Fork of ${existingShader.title}`); + setTitle(existingShader.title); + setDescription(existingShader.description || ''); setShaderType(existingShader.shader_type); setTags(existingShader.tags?.join(', ') || ''); - } - }, [existingShader]); - // Debounced live preview update + // If we own it, we're editing; otherwise forking + if (user && existingShader.author_id === user.id) { + setEditingExisting(true); + } else { + setTitle(`Fork of ${existingShader.title}`); + setEditingExisting(false); + } + } + }, [existingShader, user]); + + // ── Drag handle for resizable pane ────────────────────── + const handleMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + isDragging.current = true; + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + }, []); + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging.current || !containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + const pct = ((e.clientX - rect.left) / rect.width) * 100; + setEditorWidth(Math.max(20, Math.min(80, pct))); + }; + const handleMouseUp = () => { + isDragging.current = false; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, []); + + // ── Debounced live preview ────────────────────────────── const handleCodeChange = useCallback((value: string) => { setCode(value); + setSavedStatus(null); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { setLiveCode(value); }, 400); }, []); - const handleSubmit = async () => { + // ── Save / Publish ───────────────────────────────────── + const handleSave = async (publishStatus: 'draft' | 'published') => { if (!isAuthenticated()) { navigate('/login'); return; @@ -88,21 +137,44 @@ export default function Editor() { setSubmitting(true); setSubmitError(''); + const payload = { + title, + description, + glsl_code: code, + tags: tags.split(',').map(t => t.trim()).filter(Boolean), + shader_type: shaderType, + status: publishStatus, + is_public: publishStatus === 'published', + }; + try { - const { data } = await api.post('/shaders', { - title, - description, - glsl_code: code, - tags: tags.split(',').map(t => t.trim()).filter(Boolean), - shader_type: shaderType, - }); - navigate(`/shader/${data.id}`); + if (editingExisting && id) { + // Update existing shader + const { data } = await api.put(`/shaders/${id}`, { + ...payload, + change_note: publishStatus === 'published' ? 'Updated' : undefined, + }); + setSavedStatus(publishStatus === 'draft' ? 'Draft saved' : 'Published'); + if (publishStatus === 'published') { + setTimeout(() => navigate(`/shader/${data.id}`), 800); + } + } else { + // Create new shader + const { data } = await api.post('/shaders', payload); + if (publishStatus === 'published') { + navigate(`/shader/${data.id}`); + } else { + // Redirect to editor with the new ID so subsequent saves are updates + setSavedStatus('Draft saved'); + navigate(`/editor/${data.id}`, { replace: true }); + } + } } catch (err: any) { const detail = err.response?.data?.detail; if (typeof detail === 'object' && detail.errors) { setSubmitError(detail.errors.join('\n')); } else { - setSubmitError(detail || 'Submission failed'); + setSubmitError(detail || 'Save failed'); } } finally { setSubmitting(false); @@ -119,7 +191,7 @@ export default function Editor() { value={title} onChange={(e) => setTitle(e.target.value)} className="bg-transparent text-lg font-medium text-gray-100 focus:outline-none - border-b border-transparent focus:border-fracta-500 transition-colors" + border-b border-transparent focus:border-fracta-500 transition-colors w-64" placeholder="Shader title..." /> + {editingExisting && existingShader && ( + + v{existingShader.current_version} + + )}
@@ -136,8 +213,18 @@ export default function Editor() { ⚠ {compileError.split('\n')[0]} )} + {savedStatus && ( + {savedStatus} + )} +
)} - {/* Split pane: editor + preview */} -
+ {/* Split pane: editor + drag handle + preview */} +
{/* Code editor */} -
+
fragment.glsl @@ -211,8 +298,19 @@ export default function Editor() { />
+ {/* Drag handle */} +
+
{/* Wider hit area */} +
+
+ {/* Live preview */} -
+
('all'); + + if (!isAuthenticated()) { + navigate('/login'); + return null; + } + + const { data: shaders = [], isLoading } = useQuery({ + queryKey: ['my-shaders', tab], + queryFn: async () => { + const params: any = { limit: 100 }; + if (tab !== 'all') params.status = tab; + const { data } = await api.get('/shaders/mine', { params }); + return data; + }, + }); + + const archiveMutation = useMutation({ + mutationFn: async (id: string) => { + await api.put(`/shaders/${id}`, { status: 'archived' }); + }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['my-shaders'] }), + }); + + const publishMutation = useMutation({ + mutationFn: async (id: string) => { + await api.put(`/shaders/${id}`, { status: 'published', is_public: true }); + }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['my-shaders'] }), + }); + + const deleteMutation = useMutation({ + mutationFn: async (id: string) => { + await api.delete(`/shaders/${id}`); + }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['my-shaders'] }), + }); + + const counts = { + all: shaders.length, + draft: shaders.filter((s: any) => s.status === 'draft').length, + published: shaders.filter((s: any) => s.status === 'published').length, + archived: shaders.filter((s: any) => s.status === 'archived').length, + }; + + return ( +
+
+
+

My Shaders

+

+ Your workspace — drafts, published shaders, and version history. +

+
+ + New Shader +
+ + {/* Status tabs */} +
+ {(['all', 'draft', 'published', 'archived'] as StatusTab[]).map((s) => ( + + ))} +
+ + {/* Shader list */} + {isLoading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+
+
+
+
+
+
+ ))} +
+ ) : shaders.length > 0 ? ( +
+ {shaders.map((shader: any) => ( +
+ +
+ + {/* Status badge */} + + {shader.status} + + {/* Version badge */} + + v{shader.current_version} + +
+ +
+ +

+ {shader.title} +

+ +
+ + {new Date(shader.updated_at).toLocaleDateString()} · {shader.shader_type} + +
+ {shader.status === 'draft' && ( + <> + + Edit + + + + )} + {shader.status === 'published' && ( + <> + + Edit + + + + )} + {shader.status === 'archived' && ( + <> + + + + )} +
+
+
+
+ ))} +
+ ) : ( +
+

No {tab === 'all' ? '' : tab + ' '}shaders yet

+ Create Your First Shader +
+ )} +
+ ); +}