feat(examples): add 10 WebGL example templates (#5312)

* feat(examples): add 10 WebGL example templates

Adds 10 first-party powered-preview WebGL examples under plugins/_official/examples,
each a self-contained dir (SKILL.md + example.html + open-design.json, self-contained i18n):

C-tier — faithful Codrops ports (MIT + credit + LICENSE; original imagery):
  webgl-depth-gallery, webgl-pixel-reveal-gallery,
  webgl-horizontal-parallax, webgl-distortion-grain
D-tier — original self-contained WebGL2 shader heroes (no third-party assets):
  webgl-caustic-pool, webgl-halftone-drift, webgl-holographic-foil,
  webgl-voronoi-cells, webgl-aurora-veil, webgl-liquid-iridescence

All changed files are <1MB (pass the blob-size gate). The two large mirror
showcases (oryzo, opendesign) are deferred: they exceed the 1MB blob guard and
need their heavy assets moved to repository-assets R2 (per the gate's guidance).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(examples): stop shader setup after WebGL2 fallback message

Wrap each pure-shader example script in an IIFE and return early when
getContext('webgl2') is unavailable, so unsupported environments settle
on the fallback message instead of throwing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Tuola Ge <gexingli@refly.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
XingliGe
2026-07-08 17:29:38 +00:00
committed by GitHub
co-authored by Claude Fable 5 Tuola Ge
parent 3b4cba7033
commit 7d005a17c3
56 changed files with 3094 additions and 0 deletions
@@ -0,0 +1,43 @@
---
name: webgl-aurora-veil
description: |
A self-contained WebGL2 hero: layered aurora light curtains warped over a night sky scattered with stars; move the cursor to sway the veil.
triggers:
- "aurora veil"
- "webgl hero"
od:
mode: prototype
platform: web
scenario: design
preview:
type: html
entry: index.html
reload: debounce-100
design_system:
requires: false
craft:
requires: [animation-discipline]
---
# Aurora Veil
Produce a single self-contained `index.html` — A self-contained WebGL2 hero: layered aurora light curtains warped over a night sky scattered with stars; move the cursor to sway the veil.
## Why this is a powered artifact
Open Design detects `getContext('webgl2')` / heavy WebGL and renders this file in **powered preview** (a cross-origin-isolated iframe). The full GPU + scroll pipeline runs; no opaque-sandbox workarounds are needed.
## Resource map
```
webgl-aurora-veil/
├── SKILL.md ← you're reading this
├── example.html ← the complete, working artifact (READ FIRST)
└── (assets, if any)
```
## Credits / attribution
- Original, self-contained (no third-party assets).
Keep any bundled LICENSE and on-screen credit intact. Replace imagery only with license-clean assets (original / AI, Lummi.ai, Unsplash/Pexels — never scraped imagery).
@@ -0,0 +1,102 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aurora Veil — Open Design</title>
<!--
Self-contained WebGL2 hero. Layered aurora curtains: vertical light sheets
warped by FBM ride over a night-sky gradient with a scatter of stars; the
curtains sway toward the pointer. No meshes, no textures, no build step.
-->
<style>
:root{ color-scheme:dark; }
html,body{ margin:0; height:100%; overflow:hidden; background:#03040c;
font-family:'Albert Sans',ui-sans-serif,system-ui,-apple-system,sans-serif; color:#eafff6; }
#gl{ position:fixed; inset:0; width:100%; height:100%; display:block; }
.overlay{ position:fixed; inset:0; z-index:2; pointer-events:none; display:flex; flex-direction:column; justify-content:space-between; padding:5vw; }
.top{ display:flex; justify-content:space-between; font-size:clamp(11px,1vw,14px); letter-spacing:0.32em; text-transform:uppercase; color:rgba(234,255,246,0.72); }
.top .dot{ width:8px; height:8px; border-radius:50%; background:#7cffb2; box-shadow:0 0 14px #7cffb2; display:inline-block; margin-right:10px; vertical-align:middle; }
h1{ margin:0; font-size:clamp(44px,11vw,168px); line-height:0.86; font-weight:800; letter-spacing:-0.04em; mix-blend-mode:screen; }
h1 .thin{ font-weight:300; }
.bottom{ display:flex; justify-content:space-between; align-items:flex-end; gap:4vw; }
.sub{ font-size:clamp(14px,1.5vw,19px); max-width:42ch; color:rgba(234,255,246,0.82); line-height:1.45; margin:0; }
.badge{ font-size:11px; letter-spacing:0.28em; text-transform:uppercase; color:rgba(234,255,246,0.65); border:1px solid rgba(234,255,246,0.24); padding:9px 15px; border-radius:999px; backdrop-filter:blur(8px); white-space:nowrap; }
</style>
</head>
<body>
<canvas id="gl"></canvas>
<div class="overlay">
<div class="top"><span><span class="dot"></span>Open Design · WebGL Preview</span><span>Real-time · GPU</span></div>
<div class="bottom">
<div><h1>AURORA<br><span class="thin">VEIL</span></h1>
<p class="sub">Layered light curtains warped over a night sky, scattered with stars. Move the cursor to sway the veil.</p></div>
<div class="badge">Sway the veil ↗</div>
</div>
</div>
<script>
(() => {
const canvas=document.getElementById('gl');
const gl=canvas.getContext('webgl2',{antialias:true});
if (!gl) { document.body.innerHTML='<p style="color:#fff;padding:2rem;font-family:sans-serif">WebGL2 not available.</p>'; return; }
const VERT=`#version 300 es
void main(){ vec2 p=vec2((gl_VertexID<<1)&2, gl_VertexID&2); gl_Position=vec4(p*2.0-1.0,0.0,1.0); }`;
const FRAG=`#version 300 es
precision highp float;
out vec4 o;
uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse;
float hash(vec2 p){ return fract(sin(dot(p,vec2(41.3,289.1)))*43758.5453); }
float noise(vec2 p){ vec2 i=floor(p),f=fract(p); vec2 u=f*f*(3.0-2.0*f);
return mix(mix(hash(i),hash(i+vec2(1,0)),u.x),mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),u.x),u.y); }
float fbm(vec2 p){ float v=0.0,a=0.5; for(int i=0;i<5;i++){ v+=a*noise(p); p=p*1.9+3.0; a*=0.55; } return v; }
vec3 auroraPal(float t){ // green → teal → violet
return mix(mix(vec3(0.10,0.9,0.45), vec3(0.15,0.8,0.85), smoothstep(0.0,0.5,t)),
vec3(0.6,0.35,0.95), smoothstep(0.5,1.0,t));
}
void main(){
vec2 uv=gl_FragCoord.xy/u_res;
vec2 p=(gl_FragCoord.xy-0.5*u_res)/u_res.y;
float sway=(u_mouse.x-0.5)*1.2;
// night sky gradient
vec3 col=mix(vec3(0.02,0.03,0.09), vec3(0.03,0.06,0.16), uv.y);
// stars (upper sky) — sparse points, not streaks
vec2 sc=floor(gl_FragCoord.xy/3.0);
float star=step(0.9975, hash(sc))*pow(hash(sc+7.0),2.0)*smoothstep(0.3,1.0,uv.y);
col+=vec3(0.9,0.95,1.0)*star;
// aurora curtains: soft sheets whose centre SNAKES horizontally with height + time
float t=u_time*0.15;
for(int i=0;i<3;i++){
float fi=float(i);
float base=(fi-1.0)*0.55 + sway*(0.35+fi*0.2);
float cx=base + sin(p.y*1.6 + t*2.0 + fi*2.1)*0.22 + (fbm(vec2(p.y*1.2+fi*3.0, t*1.5))-0.5)*0.7;
float w=0.15 + 0.09*fbm(vec2(p.y*3.0+fi, t));
float band=exp(-pow((p.x-cx)/w, 2.0));
float vert=smoothstep(-0.95,0.8,p.y) * (0.45+0.55*fbm(vec2(p.x*5.0+fi*4.0, p.y*2.6 - t*2.5)));
float ribbon=band*vert;
float hue=fract(0.12 + fi*0.28 + p.y*0.16 + t*0.3);
col += auroraPal(hue) * ribbon * (1.0 - fi*0.14);
}
// ground glow + vignette
col *= 0.7+0.5*smoothstep(1.5,0.1,length(p*vec2(0.6,1.0)));
col=col/(col+0.8);
col+=(hash(gl_FragCoord.xy+u_time)-0.5)*0.02;
o=vec4(pow(max(col,0.0),vec3(0.9)),1.0);
}`;
function sh(t,s){const x=gl.createShader(t);gl.shaderSource(x,s);gl.compileShader(x);if(!gl.getShaderParameter(x,gl.COMPILE_STATUS))throw gl.getShaderInfoLog(x);return x;}
const pr=gl.createProgram();gl.attachShader(pr,sh(gl.VERTEX_SHADER,VERT));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,FRAG));gl.linkProgram(pr);
if(!gl.getProgramParameter(pr,gl.LINK_STATUS))throw gl.getProgramInfoLog(pr);
gl.useProgram(pr);
const uRes=gl.getUniformLocation(pr,'u_res'),uTime=gl.getUniformLocation(pr,'u_time'),uMouse=gl.getUniformLocation(pr,'u_mouse');
let mouse=[0.5,0.5],target=[0.5,0.5];
addEventListener('pointermove',e=>{target=[e.clientX/innerWidth,1.0-e.clientY/innerHeight];});
function resize(){const d=Math.min(devicePixelRatio||1,2);canvas.width=innerWidth*d|0;canvas.height=innerHeight*d|0;gl.viewport(0,0,canvas.width,canvas.height);}
addEventListener('resize',resize);resize();
const t0=performance.now();
function frame(now){ mouse[0]+=(target[0]-mouse[0])*0.05; mouse[1]+=(target[1]-mouse[1])*0.05;
gl.uniform2f(uRes,canvas.width,canvas.height); gl.uniform1f(uTime,(now-t0)/1000); gl.uniform2f(uMouse,mouse[0],mouse[1]);
gl.drawArrays(gl.TRIANGLES,0,3); requestAnimationFrame(frame); }
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
@@ -0,0 +1,93 @@
{
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
"specVersion": "1.0.0",
"name": "example-webgl-aurora-veil",
"title": "Aurora Veil",
"title_i18n": {
"zh-CN": "极光帘幕",
"zh-TW": "極光簾幕",
"ja": "オーロラ・ヴェール",
"ko": "오로라 베일",
"de": "Aurora-Schleier",
"fr": "Voile aurore",
"ru": "Завеса полярного сияния",
"es": "Velo de aurora",
"pt-BR": "Véu de aurora",
"en": "Aurora Veil"
},
"version": "0.1.0",
"description": "A self-contained WebGL2 hero: layered aurora light curtains warped over a night sky scattered with stars; move the cursor to sway the veil.",
"description_i18n": {
"zh-CN": "自包含 WebGL2 主视觉:夜空之上扭曲的分层极光帘幕,点缀星光;移动光标摆动帘幕。",
"zh-TW": "自包含 WebGL2 主視覺:夜空之上扭曲的分層極光簾幕,點綴星光;移動游標擺動簾幕。",
"en": "A self-contained WebGL2 hero: layered aurora light curtains warped over a night sky scattered with stars; move the cursor to sway the veil."
},
"license": "MIT",
"author": {
"name": "Open Design",
"url": "https://github.com/nexu-io"
},
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/_official/examples/webgl-aurora-veil",
"tags": [
"example",
"first-party",
"hero",
"webgl",
"webgl2",
"shader",
"aurora",
"night",
"generative",
"powered-preview",
"untitled"
],
"compat": {
"agentSkills": [
{
"path": "./SKILL.md"
}
]
},
"od": {
"kind": "scenario",
"taskKind": "new-generation",
"mode": "prototype",
"scenario": "design",
"surface": "web",
"preview": {
"type": "html",
"entry": "./example.html"
},
"useCase": {
"query": {
"en": "Build a self-contained WebGL2 hero of layered aurora curtains over a starry night sky, cursor-swayed.",
"zh-CN": "构建自包含 WebGL2 主视觉:夜空之上分层极光帘幕加星光,光标可摆动。"
}
},
"context": {
"skills": [
{
"path": "./SKILL.md"
}
],
"designSystem": {
"primary": true
}
},
"pipeline": {
"stages": [
{
"id": "generate",
"atoms": [
"file-write",
"live-artifact"
]
}
]
},
"capabilities": [
"prompt:inject",
"fs:write"
]
}
}
@@ -0,0 +1,43 @@
---
name: webgl-caustic-pool
description: |
A self-contained WebGL2 hero: animated water caustics woven from domain-warped ripples; click the water to drop a ripple. No meshes, no textures.
triggers:
- "caustic pool"
- "webgl hero"
od:
mode: prototype
platform: web
scenario: design
preview:
type: html
entry: index.html
reload: debounce-100
design_system:
requires: false
craft:
requires: [animation-discipline]
---
# Caustic Pool
Produce a single self-contained `index.html` — A self-contained WebGL2 hero: animated water caustics woven from domain-warped ripples; click the water to drop a ripple. No meshes, no textures.
## Why this is a powered artifact
Open Design detects `getContext('webgl2')` / heavy WebGL and renders this file in **powered preview** (a cross-origin-isolated iframe). The full GPU + scroll pipeline runs; no opaque-sandbox workarounds are needed.
## Resource map
```
webgl-caustic-pool/
├── SKILL.md ← you're reading this
├── example.html ← the complete, working artifact (READ FIRST)
└── (assets, if any)
```
## Credits / attribution
- Original, self-contained (no third-party assets).
Keep any bundled LICENSE and on-screen credit intact. Replace imagery only with license-clean assets (original / AI, Lummi.ai, Unsplash/Pexels — never scraped imagery).
@@ -0,0 +1,103 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Caustic Pool — Open Design</title>
<!--
Self-contained WebGL2 hero. Animated water caustics: layered domain-warped
ripples run a cheap refraction-network approximation, brightened into caustic
filaments. Pointer drops a radial ripple that bends the network. No meshes,
no textures, no build step, no external assets.
-->
<style>
:root{ color-scheme:dark; }
html,body{ margin:0; height:100%; overflow:hidden; background:#04121a;
font-family:'Albert Sans',ui-sans-serif,system-ui,-apple-system,sans-serif; color:#eafcff; }
#gl{ position:fixed; inset:0; width:100%; height:100%; display:block; }
.overlay{ position:fixed; inset:0; z-index:2; pointer-events:none;
display:flex; flex-direction:column; justify-content:space-between; padding:5vw; }
.top{ display:flex; justify-content:space-between; font-size:clamp(11px,1vw,14px);
letter-spacing:0.32em; text-transform:uppercase; color:rgba(234,252,255,0.72); }
.top .dot{ width:8px; height:8px; border-radius:50%; background:#7dffd6; box-shadow:0 0 14px #7dffd6;
display:inline-block; margin-right:10px; vertical-align:middle; }
h1{ margin:0; font-size:clamp(44px,11vw,170px); line-height:0.86; font-weight:800; letter-spacing:-0.04em; mix-blend-mode:overlay; }
h1 .thin{ font-weight:300; }
.bottom{ display:flex; justify-content:space-between; align-items:flex-end; gap:4vw; }
.sub{ font-size:clamp(14px,1.5vw,19px); max-width:42ch; color:rgba(234,252,255,0.8); line-height:1.45; margin:0; }
.badge{ font-size:11px; letter-spacing:0.28em; text-transform:uppercase; color:rgba(234,252,255,0.6);
border:1px solid rgba(234,252,255,0.22); padding:9px 15px; border-radius:999px; backdrop-filter:blur(8px); white-space:nowrap; }
</style>
</head>
<body>
<canvas id="gl"></canvas>
<div class="overlay">
<div class="top"><span><span class="dot"></span>Open Design · WebGL Preview</span><span>Real-time · GPU</span></div>
<div class="bottom">
<div><h1>CAUSTIC<br><span class="thin">POOL</span></h1>
<p class="sub">A refraction network woven from domain-warped ripples, brightened into living caustic filaments. Click the water to drop a ripple.</p></div>
<div class="badge">Click the water ↗</div>
</div>
</div>
<script>
(() => {
const canvas=document.getElementById('gl');
const gl=canvas.getContext('webgl2',{antialias:true});
if (!gl) { document.body.innerHTML='<p style="color:#fff;padding:2rem;font-family:sans-serif">WebGL2 not available.</p>'; return; }
const VERT=`#version 300 es
void main(){ vec2 p=vec2((gl_VertexID<<1)&2, gl_VertexID&2); gl_Position=vec4(p*2.0-1.0,0.0,1.0); }`;
const FRAG=`#version 300 es
precision highp float;
out vec4 o;
uniform vec2 u_res; uniform float u_time; uniform vec3 u_ripple; // xy=pos(0..1), z=age
#define TAU 6.28318530718
// canonical caustic network (folding-coordinate accumulation, needs the mod-tiling)
float caustic(vec2 uv){
float t=u_time*0.4+23.0;
vec2 p=mod(uv*TAU, TAU)-250.0;
vec2 i=p; float c=1.0; float inten=0.005;
for(int n=0;n<5;n++){
float tt=t*(1.0-(3.5/float(n+1)));
i=p+vec2(cos(tt-i.x)+sin(tt+i.y), sin(tt-i.y)+cos(tt+i.x));
c+=1.0/length(vec2(p.x/(sin(i.x+tt)/inten), p.y/(cos(i.y+tt)/inten)));
}
c/=5.0; c=1.17-pow(c,1.4);
return pow(abs(c),8.0);
}
void main(){
vec2 uv=gl_FragCoord.xy/u_res; uv.x*=u_res.x/u_res.y;
vec2 p=(gl_FragCoord.xy-0.5*u_res)/u_res.y;
// pointer ripple bends the sampling coords
vec2 rp=(u_ripple.xy-0.5)*vec2(u_res.x/u_res.y,1.0);
float d=length(p-rp), age=u_ripple.z;
float wave=sin(d*24.0-age*8.0)*exp(-d*3.5)*exp(-age*1.6);
uv += normalize(p-rp+1e-4)*wave*0.4;
float c=caustic(uv*1.6);
vec3 deep=vec3(0.02,0.10,0.17), shallow=vec3(0.05,0.34,0.42);
vec3 col=mix(deep,shallow, smoothstep(1.0,-0.3,length(p)));
col+=vec3(0.35,0.85,0.80)*c; // aqua caustic filaments
col+=vec3(0.85,1.0,0.95)*pow(c,1.8)*0.5; // hot cores
col*=0.6+0.6*smoothstep(1.35,0.1,length(p)); // vignette
col=col/(col+0.75);
col+=(fract(sin(dot(gl_FragCoord.xy,vec2(12.9,78.2)))*43758.5)-0.5)*0.025;
o=vec4(pow(max(col,0.0),vec3(0.85)),1.0);
}`;
function sh(t,s){const x=gl.createShader(t);gl.shaderSource(x,s);gl.compileShader(x);if(!gl.getShaderParameter(x,gl.COMPILE_STATUS))throw gl.getShaderInfoLog(x);return x;}
const pr=gl.createProgram();gl.attachShader(pr,sh(gl.VERTEX_SHADER,VERT));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,FRAG));gl.linkProgram(pr);
if(!gl.getProgramParameter(pr,gl.LINK_STATUS))throw gl.getProgramInfoLog(pr);
gl.useProgram(pr);
const uRes=gl.getUniformLocation(pr,'u_res'),uTime=gl.getUniformLocation(pr,'u_time'),uRip=gl.getUniformLocation(pr,'u_ripple');
let ripple=[0.5,0.5,999];
addEventListener('pointerdown',e=>{ ripple=[e.clientX/innerWidth, 1.0-e.clientY/innerHeight, 0]; });
function resize(){const d=Math.min(devicePixelRatio||1,2);canvas.width=innerWidth*d|0;canvas.height=innerHeight*d|0;gl.viewport(0,0,canvas.width,canvas.height);}
addEventListener('resize',resize);resize();
const t0=performance.now();
function frame(now){ ripple[2]+=1/60;
gl.uniform2f(uRes,canvas.width,canvas.height); gl.uniform1f(uTime,(now-t0)/1000); gl.uniform3f(uRip,ripple[0],ripple[1],ripple[2]);
gl.drawArrays(gl.TRIANGLES,0,3); requestAnimationFrame(frame); }
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
@@ -0,0 +1,93 @@
{
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
"specVersion": "1.0.0",
"name": "example-webgl-caustic-pool",
"title": "Caustic Pool",
"title_i18n": {
"zh-CN": "水面焦散",
"zh-TW": "水面焦散",
"ja": "コースティック・プール",
"ko": "코스틱 풀",
"de": "Kaustik-Pool",
"fr": "Bassin de caustiques",
"ru": "Каустический бассейн",
"es": "Piscina de cáusticas",
"pt-BR": "Piscina de cáusticas",
"en": "Caustic Pool"
},
"version": "0.1.0",
"description": "A self-contained WebGL2 hero: animated water caustics woven from domain-warped ripples; click the water to drop a ripple. No meshes, no textures.",
"description_i18n": {
"zh-CN": "自包含 WebGL2 主视觉:由域扭曲涟漪织成的动态水面焦散;点击水面掉涟漪。无网格、无贴图。",
"zh-TW": "自包含 WebGL2 主視覺:由域扭曲漣漪織成的動態水面焦散;點擊水面掉漣漪。無網格、無貼圖。",
"en": "A self-contained WebGL2 hero: animated water caustics woven from domain-warped ripples; click the water to drop a ripple. No meshes, no textures."
},
"license": "MIT",
"author": {
"name": "Open Design",
"url": "https://github.com/nexu-io"
},
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/_official/examples/webgl-caustic-pool",
"tags": [
"example",
"first-party",
"hero",
"webgl",
"webgl2",
"shader",
"caustics",
"water",
"generative",
"powered-preview",
"untitled"
],
"compat": {
"agentSkills": [
{
"path": "./SKILL.md"
}
]
},
"od": {
"kind": "scenario",
"taskKind": "new-generation",
"mode": "prototype",
"scenario": "design",
"surface": "web",
"preview": {
"type": "html",
"entry": "./example.html"
},
"useCase": {
"query": {
"en": "Build a self-contained WebGL2 hero of animated water caustics from domain-warped ripples, with a click-to-ripple interaction.",
"zh-CN": "构建自包含 WebGL2 主视觉:域扭曲涟漪织成的动态水面焦散,点击掉涟漪。"
}
},
"context": {
"skills": [
{
"path": "./SKILL.md"
}
],
"designSystem": {
"primary": true
}
},
"pipeline": {
"stages": [
{
"id": "generate",
"atoms": [
"file-write",
"live-artifact"
]
}
]
},
"capabilities": [
"prompt:inject",
"fs:write"
]
}
}
@@ -0,0 +1,23 @@
MIT License
Effect © Houmahani Kane / Codrops. Ported to a single self-contained file and packaged for Open Design.
Original: https://github.com/houmahani/codrops-depth-gallery (declared MIT).
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Sample imagery from Lummi.ai (free commercial license).
@@ -0,0 +1,44 @@
---
name: webgl-depth-gallery
description: |
A scroll-reactive 3D image gallery in Three.js: Z-stacked images crossfade over per-image mood backgrounds with velocity breath, a glowing cursor trail and an editorial CMYK/RGB/HEX/PMS color card.
triggers:
- "depth gallery"
- "webgl gallery"
od:
mode: prototype
platform: web
scenario: design
preview:
type: html
entry: index.html
reload: debounce-100
design_system:
requires: false
craft:
requires: [animation-discipline]
---
# Depth Gallery
Produce a single self-contained `index.html` — A scroll-reactive 3D image gallery in Three.js: Z-stacked images crossfade over per-image mood backgrounds with velocity breath, a glowing cursor trail and an editorial CMYK/RGB/HEX/PMS color card.
## Why this is a powered artifact
Open Design detects `getContext('webgl2')` / heavy WebGL and renders this file in **powered preview** (a cross-origin-isolated iframe). The full GPU + scroll pipeline runs; no opaque-sandbox workarounds are needed.
## Resource map
```
webgl-depth-gallery/
├── SKILL.md ← you're reading this
├── example.html ← the complete, working artifact (READ FIRST)
└── (assets, if any)
```
## Credits / attribution
- effect: Houmahani Kane / Codrops (MIT)
- imagery: Lummi.ai
Keep any bundled LICENSE and on-screen credit intact. Replace imagery only with license-clean assets (original / AI, Lummi.ai, Unsplash/Pexels — never scraped imagery).
@@ -0,0 +1,415 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Atmospheric Depth Gallery — Open Design</title>
<!--
Faithful self-contained port of Houmahani Kane's "Atmospheric Depth Gallery"
(Codrops, MIT). Original: https://github.com/houmahani/codrops-depth-gallery
Imagery: Lummi.ai (free commercial license). De-Vited into one file for the
Open Design template sandbox: Three.js via CDN, shaders inlined, images local.
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;}
:root{ font-size:12px; --color-text:#fff;
--ui-font:'IBM Plex Mono','SFMono-Regular',Menlo,Monaco,Consolas,monospace; --ui-tracking:0.08em; }
html,body{ margin:0; height:100%; background:#fffaf0; overflow:hidden; }
body{ color:var(--color-text); font-family:var(--ui-font); text-transform:uppercase;
letter-spacing:var(--ui-tracking); font-size:0.82rem; line-height:1.2;
-webkit-font-smoothing:antialiased; transition:color 520ms ease; }
body.frame-text-dark{ --color-text:#121212; }
.webgl{ position:fixed; inset:0; width:100%; height:100%; pointer-events:auto; touch-action:none; z-index:0; }
/* frame (attribution + OD brand) */
.frame{ position:fixed; inset:0; padding:1.5rem; z-index:1000; pointer-events:none;
display:flex; justify-content:space-between; align-items:flex-start; color:var(--color-text); transition:color 520ms ease; }
.frame__title{ font-size:inherit; font-weight:600; margin:0; }
.frame .dot{ width:7px; height:7px; border-radius:50%; background:#b8ff3c; box-shadow:0 0 12px #b8ff3c; display:inline-block; margin-right:8px; vertical-align:middle; }
.frame__credit{ margin:0; opacity:0.78; font-size:0.78rem; font-weight:500; text-align:right; line-height:1.5; }
.frame a{ color:inherit; text-decoration:underline; pointer-events:auto; }
/* label overlay (ported from canvas.css) */
.plane-label-overlay{ position:fixed; inset:0; z-index:80; pointer-events:none;
transition:opacity 260ms ease; color:rgba(244,244,244,0.92); opacity:0; }
.plane-label-overlay__left{ position:absolute; left:clamp(2.5rem,8vw,12rem); top:50%; display:grid; gap:0.75rem; }
.plane-label-overlay__index{ margin:0; font-size:9px; }
.plane-label-overlay__chip{ width:18px; height:18px; border-radius:50%; display:inline-block; box-shadow:0 0 0 1px rgba(255,255,255,0.14); }
.plane-label-overlay__right{ position:absolute; right:clamp(2.5rem,7vw,10rem); top:50%; }
.plane-label-card{ width:min(28vw,360px); font-family:var(--ui-font); text-transform:uppercase;
letter-spacing:var(--ui-tracking); line-height:1.15; color:inherit; }
.plane-label-card__word{ margin:0 0 0.5rem; font-size:clamp(9px,0.78vw,11px); white-space:nowrap; }
.plane-label-card__specs{ margin:0; display:grid; gap:0.25rem; }
.plane-label-card__row{ margin:0; display:grid; grid-template-columns:3.5rem 1fr; align-items:baseline; gap:0.8rem; }
.plane-label-card__row dt,.plane-label-card__row dd{ margin:0; }
.plane-label-card__row dt{ font-size:9px; }
.plane-label-card__value{ font-size:clamp(9px,0.72vw,11px); }
.hint{ position:fixed; left:50%; bottom:1.4rem; transform:translateX(-50%); z-index:80;
font-size:9px; opacity:0.55; pointer-events:none; color:var(--color-text); transition:color 520ms ease; }
</style>
</head>
<body>
<canvas class="webgl"></canvas>
<div class="frame">
<h1 class="frame__title"><span class="dot"></span>Open Design · WebGL Preview</h1>
<p class="frame__credit">
Effect: <a href="https://github.com/houmahani/codrops-depth-gallery" target="_blank">Houmahani Kane / Codrops</a> (MIT)<br>
Imagery: <a href="https://lummi.ai" target="_blank">Lummi.ai</a>
</p>
</div>
<div class="hint">Scroll ↓ · faster = more motion</div>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
// ---------- gallery data (from galleryData.js — Lummi imagery) ----------
const galleryPlaneData = [
{ fallbackColor:'#feca4f', accentColor:'#feca4f', textureSrc:'flower-01.webp', position:{x:-0.9,y:0}, backgroundColor:'#fffaf0', blob1Color:'#ffdf94', blob2Color:'#fce7c4', label:{word:'golden', pms:'PMS 135 C', color:'#2e2e2e'} },
{ fallbackColor:'#80455a', accentColor:'#80455a', textureSrc:'flower-02.webp', position:{x: 0.8,y:0}, backgroundColor:'#fffaf0', blob1Color:'#d29a41', blob2Color:'#bb96af', label:{word:'violet', pms:'PMS 4985 C', color:'#2e2e2e'} },
{ fallbackColor:'#fa7b71', accentColor:'#fa7b71', textureSrc:'flower-03.webp', position:{x:-0.7,y:0}, backgroundColor:'#5f81ab', blob1Color:'#f88b8d', blob2Color:'#cfbbdd', label:{word:'afterglow', pms:'PMS 170 C', color:'#f4f4f4'} },
{ fallbackColor:'#3c72c6', accentColor:'#3c72c6', textureSrc:'flower-04.webp', position:{x: 1.0,y:0}, backgroundColor:'#5b9bc2', blob1Color:'#ffaa00', blob2Color:'#00e1ff', label:{word:'cobalt', pms:'PMS 660 C', color:'#f4f4f4'} },
{ fallbackColor:'#fdd895', accentColor:'#fdd895', textureSrc:'flower-05.webp', position:{x:-0.7,y:0}, backgroundColor:'#7d936e', blob1Color:'#fdd895', blob2Color:'#a5b599', label:{word:'meadow', pms:'PMS 7507 C', color:'#f4f4f4'} },
];
const PLANE_GAP = 5, clamp = THREE.MathUtils.clamp, lerp = THREE.MathUtils.lerp;
// ---------- renderer / camera / scenes ----------
const canvas = document.querySelector('.webgl');
const renderer = new THREE.WebGLRenderer({ canvas, antialias:true });
renderer.setPixelRatio(Math.min(devicePixelRatio||1,2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.autoClear = false;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(0,0,6);
// ---------- background (ported Background/index.js + fragment.glsl) ----------
const bgScene = new THREE.Scene();
const bgCam = new THREE.OrthographicCamera(-1,1,1,-1,0,1);
const bg = {
backgroundColor:new THREE.Color('#FBE8CD'), blob1:new THREE.Color('#FFD56D'), blob2:new THREE.Color('#5D816A'),
nBg:new THREE.Color(), nB1:new THREE.Color(), nB2:new THREE.Color(),
baseBlobRadius:0.65, secondaryRatio:0.78, baseStrength:0.9, noise:0.04,
depthToRadius:0.08, velToStrength:0.1, smooth:0.1, mDepth:0, mVel:0, sDepth:0, sVel:0,
};
const bgMat = new THREE.ShaderMaterial({
depthTest:false, depthWrite:false,
uniforms:{ uBackgroundColor:{value:bg.backgroundColor}, uBlob1Color:{value:bg.blob1}, uBlob2Color:{value:bg.blob2},
uNoiseStrength:{value:bg.noise}, uBlobRadius:{value:bg.baseBlobRadius}, uBlobRadiusSecondary:{value:bg.baseBlobRadius*bg.secondaryRatio},
uBlobStrength:{value:bg.baseStrength}, uTime:{value:0}, uVelocityIntensity:{value:0} },
vertexShader:`varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,
fragmentShader:`varying vec2 vUv;
uniform vec3 uBackgroundColor,uBlob1Color,uBlob2Color; uniform float uNoiseStrength,uBlobRadius,uBlobRadiusSecondary,uBlobStrength,uTime,uVelocityIntensity;
float random(vec2 c){ return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453123); }
void main(){
vec3 color=uBackgroundColor;
float t=uTime*0.00028;
vec2 b1=vec2(0.50+sin(t*1.0)*0.13+sin(t*1.618)*0.05, 0.48+cos(t*0.794)*0.09+cos(t*1.272)*0.03);
vec2 b2=vec2(0.35+cos(t*0.927)*0.11+cos(t*1.414)*0.04, 0.55+sin(t*1.175)*0.07+sin(t*0.618)*0.03);
float f1=smoothstep(uBlobRadius,0.0,distance(vUv,b1));
float f2=smoothstep(uBlobRadiusSecondary,0.0,distance(vUv,b2));
vec3 c1=mix(uBlob1Color,uBackgroundColor,0.35);
vec3 c2=mix(uBlob2Color,uBackgroundColor,0.35);
color=mix(color,c1,f1*uBlobStrength);
color=mix(color,c2,f2*uBlobStrength);
color+=uVelocityIntensity*0.10;
color+=(random(vUv*vec2(1387.13,947.91))-0.5)*uNoiseStrength;
gl_FragColor=vec4(clamp(color,0.0,1.0),1.0);
}`,
});
bgScene.add(new THREE.Mesh(new THREE.PlaneGeometry(2,2), bgMat));
function bgSetMoodBlend(cur,next,blend){
const b=clamp(blend,0,1);
if(!next||b<=0){ bg.backgroundColor.set(cur.background); bg.blob1.set(cur.blob1); bg.blob2.set(cur.blob2); }
else{
bg.backgroundColor.set(cur.background).lerp(bg.nBg.set(next.background),b);
bg.blob1.set(cur.blob1).lerp(bg.nB1.set(next.blob1),b);
bg.blob2.set(cur.blob2).lerp(bg.nB2.set(next.blob2),b);
}
bgMat.uniforms.uBackgroundColor.value.copy(bg.backgroundColor);
bgMat.uniforms.uBlob1Color.value.copy(bg.blob1);
bgMat.uniforms.uBlob2Color.value.copy(bg.blob2);
}
function bgUpdate(time){
bg.sDepth=lerp(bg.sDepth,bg.mDepth,bg.smooth); bg.sVel=lerp(bg.sVel,bg.mVel,bg.smooth);
bgMat.uniforms.uTime.value=time; bgMat.uniforms.uVelocityIntensity.value=bg.sVel;
const r=clamp(bg.baseBlobRadius+bg.sDepth*bg.depthToRadius,0.05,1);
const s=clamp(bg.baseStrength+bg.sVel*bg.velToStrength,0,1);
bgMat.uniforms.uBlobRadius.value=r; bgMat.uniforms.uBlobRadiusSecondary.value=r*bg.secondaryRatio; bgMat.uniforms.uBlobStrength.value=s;
}
// ---------- gallery (ported Gallery.js) ----------
const planes=[];
const geom=new THREE.PlaneGeometry(3,3);
const loader=new THREE.TextureLoader();
galleryPlaneData.forEach((d,i)=>{
const tex=loader.load(d.textureSrc, t=>{ t.colorSpace=THREE.SRGBColorSpace; t.anisotropy=4;
const img=t.image; if(img&&img.width) m.userData.aspectRatio=img.width/img.height; m.scale.x=1*m.userData.aspectRatio; });
const mat=new THREE.MeshBasicMaterial({ color:'#ffffff', map:tex, side:THREE.DoubleSide, transparent:true, depthWrite:false, opacity:i===0?1:0 });
const m=new THREE.Mesh(geom,mat);
m.userData={ base:d.position, accent:d.accentColor, bgc:d.backgroundColor, b1:d.blob1Color, b2:d.blob2Color, label:d.label, aspectRatio:1 };
m.position.set(d.position.x, 0, -i*PLANE_GAP);
scene.add(m); planes.push(m);
});
const nearestZ=0, deepestZ=-(planes.length-1)*PLANE_GAP;
function moodByIndex(i){ if(i<0||i>=planes.length) return null; const u=planes[i].userData; return {background:u.bgc, blob1:u.b1, blob2:u.b2}; }
function planeBlend(cameraZ, sampleOffset){
const sampled=cameraZ - PLANE_GAP*sampleOffset;
const nd=clamp((nearestZ - sampled)/PLANE_GAP, 0, planes.length-1);
const ci=Math.floor(nd), ni=Math.min(ci+1,planes.length-1);
return { ci, ni, blend:nd-ci };
}
// parallax/breath state
const pointerT=new THREE.Vector2(), pointerC=new THREE.Vector2();
let breathI=0, driftC=0;
addEventListener('pointermove', e=>{ pointerT.set((e.clientX/innerWidth)*2-1, -((e.clientY/innerHeight)*2-1)); }, {passive:true});
addEventListener('pointerleave', ()=>pointerT.set(0,0));
function galleryUpdate(cameraZ, vel, velMax){
// visibility crossfade
const { ci, ni, blend }=planeBlend(cameraZ, 1);
planes.forEach((p,i)=>{
let target=0; if(i===ci) target=1-blend; if(i===ni) target=Math.max(target,blend);
p.material.opacity=lerp(p.material.opacity||0, target, 0.14);
});
// motion
pointerC.lerp(pointerT, 0.08);
const vN=clamp(Math.abs(vel)/Math.max(velMax,0.0001),0,1);
const drift=clamp(vel/Math.max(velMax,0.0001),-1,1);
breathI=lerp(breathI, clamp(vN*1.1,0,1), 0.14);
driftC=lerp(driftC, drift, 0.05);
planes.forEach((p,i)=>{
const op=p.material.opacity||0;
const depthInf=1+i*0.05, pInf=op*depthInf;
p.position.x=p.userData.base.x + pointerC.x*0.16*pInf;
p.position.y=pointerC.y*0.08*pInf + driftC*0.05;
const bInf=breathI*op;
p.rotation.x=-pointerC.y*0.045*bInf;
p.rotation.y= pointerC.x*0.045*bInf;
const ar=p.userData.aspectRatio||1, pulse=1+0.03*bInf;
p.scale.x=ar*pulse; p.scale.y=pulse;
});
}
// ---------- label (ported Label.js) ----------
const ov=document.createElement('section'); ov.className='plane-label-overlay';
ov.innerHTML=`<div class="plane-label-overlay__left"><p class="plane-label-overlay__index"></p><span class="plane-label-overlay__chip"></span></div>
<article class="plane-label-card plane-label-overlay__right"><p class="plane-label-card__word"></p><dl class="plane-label-card__specs">
<div class="plane-label-card__row"><dt>CMYK</dt><dd class="v-cmyk"></dd></div>
<div class="plane-label-card__row"><dt>RGB</dt><dd class="v-rgb"></dd></div>
<div class="plane-label-card__row"><dt>HEX</dt><dd class="v-hex"></dd></div>
<div class="plane-label-card__row"><dt>PMS</dt><dd class="v-pms"></dd></div></dl></article>`;
document.body.append(ov);
const $=s=>ov.querySelector(s);
const elIdx=$('.plane-label-overlay__index'), elChip=$('.plane-label-overlay__chip'), elWord=$('.plane-label-card__word'),
elCmyk=$('.v-cmyk'), elRgb=$('.v-rgb'), elHex=$('.v-hex'), elPms=$('.v-pms');
let labelIdx=-1, frameDark=null;
function hexToRgb(h){ const n=h.replace('#',''); return { r:parseInt(n.slice(0,2),16), g:parseInt(n.slice(2,4),16), b:parseInt(n.slice(4,6),16) }; }
function rgbToCmyk({r,g,b}){ const R=r/255,G=g/255,B=b/255,k=1-Math.max(R,G,B); if(k>=0.999) return {c:0,m:0,y:0,k:100};
return { c:Math.round((1-R-k)/(1-k)*100), m:Math.round((1-G-k)/(1-k)*100), y:Math.round((1-B-k)/(1-k)*100), k:Math.round(k*100) }; }
function labelUpdate(cameraZ){
const { ci, ni, blend }=planeBlend(cameraZ, 1);
const idx = blend>=0.5 ? ni : ci;
ov.style.opacity='1';
if(idx!==labelIdx){
const u=planes[idx].userData, rgb=hexToRgb(u.accent), cmyk=rgbToCmyk(rgb);
elIdx.textContent=String(idx+1).padStart(2,'0');
elChip.style.backgroundColor=u.accent;
elWord.textContent=u.label.word;
elCmyk.textContent=`${cmyk.c}, ${cmyk.m}, ${cmyk.y}, ${cmyk.k}`;
elRgb.textContent=`${rgb.r}, ${rgb.g}, ${rgb.b}`;
elHex.textContent=u.accent.replace('#','').toUpperCase();
elPms.textContent=u.label.pms;
ov.style.color=u.label.color||'';
labelIdx=idx;
}
// frame text tone: first 2 planes have light backgrounds → dark text
const nearest = blend>=0.5 ? ni : ci;
const dark = nearest < 2;
if(dark!==frameDark){ frameDark=dark; document.body.classList.toggle('frame-text-dark', dark); }
}
// ---------- scroll (ported Scroll.js) ----------
const startZ = nearestZ + 5; // maxCameraZ
const minCameraZ = deepestZ + 5, maxCameraZ = startZ;
const F = 0.01; // scrollToWorldFactor
const minScroll=(startZ-maxCameraZ)/F, maxScroll=(startZ-minCameraZ)/F;
camera.position.z = startZ;
let scrollTarget=0, scrollCurrent=0, prevScroll=0, velocity=0;
addEventListener('wheel', e=>{ e.preventDefault();
let d=e.deltaY; if(e.deltaMode===1) d*=16; else if(e.deltaMode===2) d*=innerHeight;
scrollTarget=clamp(scrollTarget+d, minScroll, maxScroll);
}, {passive:false});
function resize(){ const w=innerWidth,h=innerHeight; renderer.setSize(w,h,false);
camera.aspect=w/h; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
// ---------- cursor trail: tapered tube along a Catmull-Rom spline (ported Trail.js) ----------
class Trail {
constructor(){
this.group=new THREE.Group(); this.points=[]; this.mesh=null;
this.minDistance=0.006; this.maxPoints=220; this.curveTension=0.67; this.curveSegments=220;
this.radialSegments=8; this.radiusHead=0.012; this.radiusTail=0.003; this.pointSmoothing=0.53;
this.maxTrimPerFrame=4; this.jumpResetDistance=999;
this.material=new THREE.MeshStandardMaterial({ color:new THREE.Color('#f6f9ff'), emissive:new THREE.Color('#ffffff'),
emissiveIntensity:1.35, roughness:0.2, metalness:0.05, transparent:true, opacity:0.51, depthWrite:false, depthTest:false });
}
get object(){ return this.group; }
addPoint(position){
if(!(position instanceof THREE.Vector3)) return;
const last=this.points[this.points.length-1]||null;
if(last && position.distanceToSquared(last)<this.minDistance*this.minDistance) return;
const next=position.clone();
if(last && next.distanceTo(last)>this.jumpResetDistance){ this.points=[next];
if(this.mesh){ this.mesh.geometry.dispose(); this.group.remove(this.mesh); this.mesh=null; } return; }
const eased=last? last.clone().lerp(next,this.pointSmoothing): next;
this.points.push(eased);
let budget=this.maxTrimPerFrame;
while(this.points.length>this.maxPoints && budget>0){ this.points.shift(); budget--; }
if(this.points.length<2) return;
const curve=new THREE.CatmullRomCurve3(this.points,false,'centripetal',this.curveTension);
const segments=Math.max(24,Math.min(this.curveSegments,this.points.length*4));
const geo=this.createTaperedTube(curve,segments,this.radiusHead,this.radiusTail);
if(!this.mesh){ this.mesh=new THREE.Mesh(geo,this.material); this.mesh.renderOrder=1200; this.group.add(this.mesh); return; }
this.mesh.geometry.dispose(); this.mesh.geometry=geo;
}
createTaperedTube(curve,segments,rHead,rTail){
const pts=curve.getSpacedPoints(segments); const R=this.radialSegments, ring=R+1;
const verts=[], idx=[]; const up=new THREE.Vector3(0,0,1), tan=new THREE.Vector3(), nor=new THREE.Vector3(),
bin=new THREE.Vector3(), off=new THREE.Vector3(), vp=new THREE.Vector3();
for(let i=0;i<pts.length;i++){
const t=i/Math.max(pts.length-1,1); const radius=rHead+(rTail-rHead)*Math.pow(t,1.5);
curve.getTangent(t,tan).normalize(); nor.crossVectors(up,tan).normalize();
if(nor.lengthSq()===0) nor.set(1,0,0); bin.crossVectors(tan,nor).normalize();
for(let j=0;j<=R;j++){ const a=(j/R)*Math.PI*2, cx=-Math.cos(a)*radius, cy=Math.sin(a)*radius;
off.copy(nor).multiplyScalar(cx).addScaledVector(bin,cy); vp.copy(pts[i]).add(off); verts.push(vp.x,vp.y,vp.z); }
}
for(let i=0;i<pts.length-1;i++){ for(let j=0;j<R;j++){ const b=i*ring+j;
idx.push(b,b+ring,b+1); idx.push(b+ring,b+ring+1,b+1); } }
const g=new THREE.BufferGeometry(); g.setAttribute('position',new THREE.Float32BufferAttribute(verts,3));
g.setIndex(idx); g.computeVertexNormals(); return g;
}
reset(){ if(this.mesh){ this.mesh.geometry.dispose(); this.group.remove(this.mesh); this.mesh=null; } this.points=[]; }
}
// ---------- head sparkle particles (ported TrailHeadParticles.js) ----------
class TrailHeadParticles {
constructor(){
this.group=new THREE.Group(); this.group.renderOrder=1300; this.isEnabled=true;
this.maxParticles=18; this.spawnPerSecond=20; this.spawnRadius=0.52; this.speedMin=0.05; this.speedMax=0.22;
this.lifeMin=0.25; this.lifeMax=0.6; this.sizeMin=0.007; this.sizeMax=0.02; this.dragPerFrame=0.94;
this.spawnAccumulator=0; this.nextSpawnIndex=0; this.geo=new THREE.SphereGeometry(1,5,4); this.particles=[];
for(let i=0;i<this.maxParticles;i++){
const m=new THREE.MeshBasicMaterial({ color:new THREE.Color('#f6f9ff'), transparent:true, opacity:0, depthWrite:false, depthTest:false });
const mesh=new THREE.Mesh(this.geo,m); mesh.visible=false; this.group.add(mesh);
this.particles.push({ mesh, velocity:new THREE.Vector3(), lifeRemaining:0, totalLife:0 });
}
}
get object(){ return this.group; }
update(dt,head,opacity=1){
const d=Math.min(Math.max(dt||0,0),0.1);
if(this.isEnabled && d>0){ this.spawnAccumulator+=d*this.spawnPerSecond; const n=Math.floor(this.spawnAccumulator); this.spawnAccumulator-=n;
for(let i=0;i<n;i++) this.spawn(head); } else this.spawnAccumulator=0;
const op=clamp(opacity,0,1), drag=Math.pow(this.dragPerFrame,d*60);
this.particles.forEach(p=>{ if(p.lifeRemaining<=0) return; p.lifeRemaining-=d;
if(p.lifeRemaining<=0){ p.lifeRemaining=0; p.mesh.visible=false; p.mesh.material.opacity=0; return; }
p.velocity.multiplyScalar(drag); p.mesh.position.addScaledVector(p.velocity,d);
p.mesh.material.opacity=(p.lifeRemaining/p.totalLife)*op*0.75; });
}
spawn(head){
const p=this.particles[this.nextSpawnIndex]; this.nextSpawnIndex=(this.nextSpawnIndex+1)%this.particles.length;
const a=Math.random()*Math.PI*2, r=Math.random()*this.spawnRadius;
p.mesh.position.set(head.x+Math.cos(a)*r, head.y+(Math.random()-0.5)*this.spawnRadius*0.6, head.z+Math.sin(a)*r);
p.mesh.scale.setScalar(lerp(this.sizeMin,this.sizeMax,Math.random())); p.mesh.visible=true;
const s=lerp(this.speedMin,this.speedMax,Math.random());
p.velocity.set((Math.random()-0.5)*s,(Math.random()-0.5)*s*0.6,(Math.random()-0.5)*s);
p.totalLife=lerp(this.lifeMin,this.lifeMax,Math.random()); p.lifeRemaining=p.totalLife; p.mesh.material.opacity=0.4;
}
}
// ---------- trail controller (ported TrailController.js) ----------
const TWO_PI=Math.PI*2;
const trail=new Trail(), sparks=new TrailHeadParticles();
scene.add(trail.object); scene.add(sparks.object);
const trailCfg={ startX:-0.96, startY:-1.05, hWidth:3, hCycles:1.85, vAmp:0.78, vCycles:2.1,
ahead:1.65, baseDepth:4.78, depthSpan:6.52, progDepth:-0.1,
minPts:14, maxPts:220, revScale:0.55, seedPts:10, seedStepZ:0.12, trimFwd:4, trimRev:32,
baseOpacity:0.51, idleOpacity:0.55, idleThresh:0.01, startBias:0.1, edge0:0.04, edge1:0.2, opSmooth:0.12, dirEps:0.0005 };
const trailHead=new THREE.Vector3();
let trailPrevProgress=null, trailPrevDir=0, trailMoved=false, trailOpacity=trailCfg.baseOpacity, trailLastTime=null;
function trailComputeHead(camZ,progress){
const p=clamp(progress,0,1);
const x=trailCfg.startX+Math.sin(p*TWO_PI*trailCfg.hCycles)*trailCfg.hWidth;
const y=trailCfg.startY+Math.sin(p*TWO_PI*trailCfg.vCycles)*trailCfg.vAmp;
const dp=trailCfg.progDepth+p*(1-trailCfg.progDepth);
const z=camZ+trailCfg.ahead-(trailCfg.baseDepth+dp*trailCfg.depthSpan);
return trailHead.set(x,y,z);
}
function trailProgress(camZ){ const range=maxCameraZ-minCameraZ;
return range>0 ? clamp((maxCameraZ-camZ)/range,0,1) : 0; }
// seed initial points (camera already at startZ)
(function(){ const start=trailComputeHead(camera.position.z,0).clone();
for(let i=trailCfg.seedPts;i>=0;i--){ const s=start.clone(); s.z-=i*trailCfg.seedStepZ; trail.addPoint(s); } })();
function trailUpdate(camZ,time){
let dt=0; if(trailLastTime!=null) dt=(time-trailLastTime)/1000; trailLastTime=time;
const progress=trailProgress(camZ);
if(progress>trailCfg.idleThresh) trailMoved=true;
const delta=trailPrevProgress==null?0:progress-trailPrevProgress;
const dir=Math.abs(delta)<=trailCfg.dirEps?0:Math.sign(delta);
const reversed=dir!==0 && trailPrevDir!==0 && dir!==trailPrevDir;
// length
const lp=dir<0?progress*trailCfg.revScale:progress;
trail.maxPoints=Math.round(lerp(trailCfg.minPts,trailCfg.maxPts,clamp(lp,0,1)));
trail.maxTrimPerFrame=dir<0?trailCfg.trimRev:trailCfg.trimFwd;
const head=trailComputeHead(camZ,progress);
// opacity
const startDist=clamp(progress+trailCfg.startBias,0,1), endDist=1-progress, edgeD=Math.min(startDist,endDist);
const edgeVis=THREE.MathUtils.smoothstep(edgeD,trailCfg.edge0,trailCfg.edge1);
const startupVis=(!trailMoved && progress<=trailCfg.idleThresh)?trailCfg.idleOpacity:0;
const target=trailCfg.baseOpacity*Math.max(edgeVis,startupVis);
trailOpacity=lerp(trailOpacity,target,trailCfg.opSmooth); trail.material.opacity=trailOpacity;
if(reversed){ trail.reset(); const lead=head.clone(); lead.z+=dir*trailCfg.seedStepZ; trail.addPoint(lead); }
trail.addPoint(head);
if(dir!==0) trailPrevDir=dir; trailPrevProgress=progress;
sparks.update(dt,head,trailOpacity);
}
// ---------- loop ----------
function frame(){
requestAnimationFrame(frame);
const time=performance.now();
// scroll + velocity
scrollCurrent=lerp(scrollCurrent, scrollTarget, 0.08);
scrollCurrent=clamp(scrollCurrent, minScroll, maxScroll);
const raw=scrollCurrent-prevScroll;
velocity=clamp(lerp(velocity, raw, 0.12), -1.5, 1.5);
if(Math.abs(velocity)<0.0001) velocity=0;
prevScroll=scrollCurrent;
camera.position.z=clamp(startZ - scrollCurrent*F, minCameraZ, maxCameraZ);
const cz=camera.position.z;
// updates
galleryUpdate(cz, velocity, 1.5);
labelUpdate(cz);
trailUpdate(cz, time);
// mood
const mb=planeBlend(cz, 1);
bgSetMoodBlend(moodByIndex(mb.ci), moodByIndex(mb.ni)||moodByIndex(mb.ci), mb.blend);
// depth + velocity → bg motion (stabilised near blend centre)
const depthProgress=clamp((nearestZ-cz)/(nearestZ-deepestZ),0,1);
const vI=clamp(Math.abs(velocity)/1.5,0,1);
const dc=Math.abs(mb.blend-0.5)*2, stab=THREE.MathUtils.smoothstep(dc,0.35,1);
bg.mDepth=depthProgress; bg.mVel=vI*stab;
bgUpdate(time);
// render: bg first, clear depth, then planes
renderer.clear(true,true,true);
renderer.render(bgScene,bgCam);
renderer.clearDepth();
renderer.render(scene,camera);
}
frame();
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@@ -0,0 +1,103 @@
{
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
"specVersion": "1.0.0",
"name": "example-webgl-depth-gallery",
"title": "Depth Gallery",
"title_i18n": {
"zh-CN": "深度画廊",
"zh-TW": "深度畫廊",
"ja": "デプスギャラリー",
"ko": "뎁스 갤러리",
"de": "Tiefen-Galerie",
"fr": "Galerie en profondeur",
"ru": "Галерея глубины",
"es": "Galería de profundidad",
"pt-BR": "Galeria de profundidade",
"en": "Depth Gallery"
},
"version": "0.1.0",
"description": "A scroll-reactive 3D image gallery in Three.js: Z-stacked images crossfade over per-image mood backgrounds with velocity breath, a glowing cursor trail and an editorial CMYK/RGB/HEX/PMS color card.",
"description_i18n": {
"zh-CN": "基于 Three.js 的滚动响应式 3D 图廊:沿 Z 轴堆叠的图片交叉淡入,配每图情绪背景、速度呼吸、发光光标拖尾与编辑感 CMYK/RGB/HEX/PMS 色卡。",
"zh-TW": "基於 Three.js 的滾動響應式 3D 圖廊:Z 軸堆疊圖片交叉淡入,配情緒背景、速度呼吸、發光游標拖尾與 CMYK/RGB/HEX/PMS 色卡。",
"en": "A scroll-reactive 3D image gallery in Three.js: Z-stacked images crossfade over per-image mood backgrounds with velocity breath, a glowing cursor trail and an editorial CMYK/RGB/HEX/PMS color card."
},
"license": "MIT",
"author": {
"name": "Open Design",
"url": "https://github.com/nexu-io"
},
"credits": [
{
"role": "effect",
"name": "Houmahani Kane / Codrops",
"license": "MIT"
},
{
"role": "imagery",
"name": "Lummi.ai"
}
],
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/_official/examples/webgl-depth-gallery",
"tags": [
"example",
"first-party",
"gallery",
"3d",
"webgl",
"three.js",
"scroll",
"images",
"powered-preview",
"untitled"
],
"compat": {
"agentSkills": [
{
"path": "./SKILL.md"
}
]
},
"od": {
"kind": "scenario",
"taskKind": "new-generation",
"mode": "prototype",
"scenario": "design",
"surface": "web",
"preview": {
"type": "html",
"entry": "./example.html"
},
"useCase": {
"query": {
"en": "Build a scroll-reactive 3D image gallery: Z-stacked images that crossfade over per-image mood backgrounds, with velocity breath, a cursor trail and a CMYK/RGB/HEX/PMS color card.",
"zh-CN": "构建滚动响应式 3D 图廊:沿 Z 轴堆叠、交叉淡入的图片,配每图情绪背景、速度呼吸、光标拖尾与 CMYK/RGB/HEX/PMS 色卡。"
}
},
"context": {
"skills": [
{
"path": "./SKILL.md"
}
],
"designSystem": {
"primary": true
}
},
"pipeline": {
"stages": [
{
"id": "generate",
"atoms": [
"file-write",
"live-artifact"
]
}
]
},
"capabilities": [
"prompt:inject",
"fs:write"
]
}
}
@@ -0,0 +1,23 @@
MIT License
Effect © Jan Kohlbach / Codrops. Ported to a single self-contained file and packaged for Open Design.
Original: https://github.com/jankohlbach/codrops-shader-on-scroll (declared MIT).
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Simplex noise © Ashima Arts (MIT). Sample imagery is original (AI-generated).
@@ -0,0 +1,45 @@
---
name: webgl-distortion-grain
description: |
A vertical gallery (Three.js) where planes bend with scroll velocity, ripple under the cursor via simplex noise, and carry a film grain.
triggers:
- "distortion & grain"
- "webgl gallery"
od:
mode: prototype
platform: web
scenario: design
preview:
type: html
entry: index.html
reload: debounce-100
design_system:
requires: false
craft:
requires: [animation-discipline]
---
# Distortion & Grain
Produce a single self-contained `index.html` — A vertical gallery (Three.js) where planes bend with scroll velocity, ripple under the cursor via simplex noise, and carry a film grain.
## Why this is a powered artifact
Open Design detects `getContext('webgl2')` / heavy WebGL and renders this file in **powered preview** (a cross-origin-isolated iframe). The full GPU + scroll pipeline runs; no opaque-sandbox workarounds are needed.
## Resource map
```
webgl-distortion-grain/
├── SKILL.md ← you're reading this
├── example.html ← the complete, working artifact (READ FIRST)
└── (assets, if any)
```
## Credits / attribution
- effect: Jan Kohlbach / Codrops (MIT)
- imagery: Original (AI-generated)
- simplex noise: Ashima Arts (MIT)
Keep any bundled LICENSE and on-screen credit intact. Replace imagery only with license-clean assets (original / AI, Lummi.ai, Unsplash/Pexels — never scraped imagery).
Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

@@ -0,0 +1,154 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Distortion &amp; Grain — Open Design</title>
<!--
Faithful single-file port of jankohlbach's "On-Scroll Image Distortion & Grain"
(Codrops, MIT). Original: https://github.com/jankohlbach/codrops-shader-on-scroll
Vertical gallery: WebGL planes synced to DOM images bend with scroll velocity
(vertex deform), distort under the cursor via simplex noise (fragment), and
carry a film grain. Native scroll + self-computed velocity (no Lenis). Three.js
via CDN; images original (generated, not the author's Midjourney set).
Simplex noise © Ashima Arts (MIT). Runs in powered preview.
-->
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Manrope:wght@700;800&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box} :root{ --bg:#0a0a0c; --ink:#ededf0; --mut:#82828a; }
html,body{ margin:0; background:var(--bg); color:var(--ink); font-family:'Manrope',system-ui,sans-serif; -webkit-font-smoothing:antialiased; }
canvas#gl{ position:fixed; inset:0; width:100%; height:100%; z-index:2; pointer-events:none; }
.frame{ position:fixed; inset:0; z-index:5; pointer-events:none; padding:1.4rem 1.6rem; display:flex; justify-content:space-between; align-items:flex-start;
font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.22em; text-transform:uppercase; color:var(--mut); }
.frame .dot{ width:7px;height:7px;border-radius:50%;background:#b8ff3c;box-shadow:0 0 12px #b8ff3c;display:inline-block;margin-right:8px;vertical-align:middle;}
.frame a{ color:var(--mut); pointer-events:auto; }
.wrap{ max-width:760px; margin:0 auto; padding:0 1.6rem; }
header.intro{ padding:24vh 0 14vh; }
header.intro h1{ margin:0; font-size:clamp(40px,7vw,96px); font-weight:800; line-height:0.92; letter-spacing:-0.03em; }
header.intro p{ margin:1.2rem 0 0; max-width:40ch; font-family:'IBM Plex Mono',monospace; font-size:13px; color:var(--mut); line-height:1.6; }
.fig{ margin:0 0 20vh; }
.fig img{ display:block; width:100%; height:auto; opacity:0; border-radius:6px; } /* WebGL draws it */
.fig .cap{ margin-top:0.8rem; font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.06em; text-transform:uppercase; color:var(--mut);
display:flex; justify-content:space-between; }
footer{ padding:0 0 22vh; font-family:'IBM Plex Mono',monospace; font-size:11px; color:var(--mut); letter-spacing:0.2em; text-transform:uppercase; text-align:center; }
</style>
</head>
<body>
<canvas id="gl"></canvas>
<div class="frame"><span><span class="dot"></span>Open Design · WebGL Preview</span>
<span>Effect: <a href="https://github.com/jankohlbach/codrops-shader-on-scroll" target="_blank">Jan Kohlbach / Codrops</a></span></div>
<div class="wrap">
<header class="intro"><h1>Scroll to<br>Distort</h1>
<p>Each frame bends with scroll velocity and ripples under your cursor — a simplex-noise distortion with a film grain, running live.</p></header>
<div id="figs"></div>
<footer>Scroll ↓ · hover the frames</footer>
</div>
<script>
const DATA=[
{ src:'dgs-img/1.jpg', name:'Fold I', idx:'01' },
{ src:'dgs-img/2.jpg', name:'Drape', idx:'02' },
{ src:'dgs-img/3.jpg', name:'Shadow', idx:'03' },
{ src:'dgs-img/4.jpg', name:'Crease', idx:'04' },
{ src:'dgs-img/5.jpg', name:'Veil', idx:'05' },
];
const figs=document.getElementById('figs');
DATA.forEach(d=>{ const f=document.createElement('figure'); f.className='fig';
f.innerHTML=`<img data-gl src="${d.src}" crossorigin="anonymous" alt="${d.name}">
<figcaption class="cap"><span>${d.idx} · ${d.name}</span><span>◍</span></figcaption>`;
figs.appendChild(f); });
</script>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
const CAMERA_POS=500;
const NOISE=`
vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}
vec2 mod289(vec2 x){return x-floor(x*(1.0/289.0))*289.0;}
vec3 permute(vec3 x){return mod289(((x*34.0)+10.0)*x);}
float snoise(vec2 v){ const vec4 C=vec4(0.211324865405187,0.366025403784439,-0.577350269189626,0.024390243902439);
vec2 i=floor(v+dot(v,C.yy)); vec2 x0=v-i+dot(i,C.xx);
vec2 i1=(x0.x>x0.y)?vec2(1.0,0.0):vec2(0.0,1.0); vec4 x12=x0.xyxy+C.xxzz; x12.xy-=i1;
i=mod289(i); vec3 p=permute(permute(i.y+vec3(0.0,i1.y,1.0))+i.x+vec3(0.0,i1.x,1.0));
vec3 m=max(0.5-vec3(dot(x0,x0),dot(x12.xy,x12.xy),dot(x12.zw,x12.zw)),0.0); m=m*m; m=m*m;
vec3 x=2.0*fract(p*C.www)-1.0; vec3 h=abs(x)-0.5; vec3 ox=floor(x+0.5); vec3 a0=x-ox;
m*=1.79284291400159-0.85373472095314*(a0*a0+h*h);
vec3 g; g.x=a0.x*x0.x+h.x*x0.y; g.yz=a0.yz*x12.xz+h.yz*x12.yw; return 130.0*dot(m,g); }
vec2 getCoverUvVert(vec2 uv, vec2 t, vec2 q){ vec2 r=vec2(min((q.x/q.y)/(t.x/t.y),1.0), min((q.y/q.x)/(t.y/t.x),1.0));
return vec2(uv.x*r.x+(1.0-r.x)*0.5, uv.y*r.y+(1.0-r.y)*0.5); }`;
const VERT=`${NOISE}
uniform float uScrollVelocity; uniform vec2 uTextureSize; uniform vec2 uQuadSize;
out vec2 vUv; out vec2 vUvCover;
vec3 deform(vec3 pos, vec2 uv){ pos.y -= sin(uv.x*3.14159265)*min(abs(uScrollVelocity),5.0)*sign(uScrollVelocity)*-0.01; return pos; }
void main(){ vUv=uv; vUvCover=getCoverUvVert(uv,uTextureSize,uQuadSize);
gl_Position=projectionMatrix*modelViewMatrix*vec4(deform(position,vUvCover),1.0); }`;
const FRAG=`${NOISE}
precision highp float;
uniform sampler2D uTexture; uniform vec2 uQuadSize; uniform float uScrollVelocity; uniform float uMouseEnter; uniform vec2 uMouseOverPos; uniform float uTime;
in vec2 vUv; in vec2 vUvCover; out vec4 outColor;
void main(){ vec2 tc=vUvCover;
float aspect=uQuadSize.y/uQuadSize.x;
float circle=1.0-distance(vec2(uMouseOverPos.x,(1.0-uMouseOverPos.y)*aspect), vec2(vUv.x,vUv.y*aspect))*15.0;
float n=snoise(gl_FragCoord.xy);
tc.x+=mix(0.0, circle*n*0.01, uMouseEnter+abs(uScrollVelocity)*0.1);
tc.y+=mix(0.0, circle*n*0.01, uMouseEnter+abs(uScrollVelocity)*0.1);
vec3 col=texture(uTexture,tc).rgb;
// film grain — cheap hash (per-pixel snoise every frame is far too heavy)
float g=fract(sin(dot(gl_FragCoord.xy, vec2(12.9898,78.233)) + uTime*57.0)*43758.5453);
col+=(g-0.5)*0.08;
outColor=vec4(col,1.0);
}`;
const canvas=document.getElementById('gl');
const renderer=new THREE.WebGLRenderer({canvas,alpha:true,antialias:true});
renderer.setSize(innerWidth,innerHeight); renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));
const scene=new THREE.Scene();
const calcFov=z=>2*Math.atan((innerHeight/2)/z)*(180/Math.PI);
const camera=new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 10, 1000);
camera.position.z=CAMERA_POS; camera.fov=calcFov(CAMERA_POS); camera.updateProjectionMatrix();
const geometry=new THREE.PlaneGeometry(1,1,24,24);
const loader=new THREE.TextureLoader();
const lerp=(a,b,t)=>a+(b-a)*t;
const store=[];
class Media{
constructor(el){ this.el=el; this.enter=0; this.over={cx:0.5,cy:0.5,tx:0.5,ty:0.5};
this.mat=new THREE.ShaderMaterial({ glslVersion:THREE.GLSL3, vertexShader:VERT, fragmentShader:FRAG, uniforms:{
uTexture:{value:null}, uTextureSize:{value:new THREE.Vector2(1,1)}, uQuadSize:{value:new THREE.Vector2(1,1)},
uScrollVelocity:{value:0}, uMouseEnter:{value:0}, uMouseOverPos:{value:new THREE.Vector2(0.5,0.5)}, uTime:{value:0} }});
this.mesh=new THREE.Mesh(geometry,this.mat); scene.add(this.mesh);
this.mat.uniforms.uTexture.value=loader.load(el.src,t=>{ t.colorSpace=THREE.SRGBColorSpace;
this.mat.uniforms.uTextureSize.value.set(t.image.width,t.image.height); });
el.addEventListener('pointerenter',()=>this.tEnter=1);
el.addEventListener('pointerleave',()=>{ this.tEnter=0; this.over.tx=0.5; this.over.ty=0.5; });
el.addEventListener('pointermove',e=>{ const b=el.getBoundingClientRect(); this.over.tx=(e.clientX-b.left)/b.width; this.over.ty=(e.clientY-b.top)/b.height; });
this.tEnter=0; this.measure();
}
measure(){ this.b=this.el.getBoundingClientRect(); this.top=this.b.top+window.scrollY; this.left=this.b.left;
this.w=this.b.width; this.h=this.b.height; this.mesh.scale.set(this.w,this.h,1);
this.mat.uniforms.uQuadSize.value.set(this.w,this.h); }
update(scrollY,vel,time){
const vy=this.top - scrollY; // viewport-space top
this.mesh.visible = vy < innerHeight+300 && vy+this.h > -300; // cull off-screen
if(!this.mesh.visible) return;
this.mesh.position.x=this.left - innerWidth/2 + this.w/2;
this.mesh.position.y=-this.top + innerHeight/2 - this.h/2 + scrollY;
this.enter=lerp(this.enter,this.tEnter,0.1);
this.over.cx=lerp(this.over.cx,this.over.tx,0.06); this.over.cy=lerp(this.over.cy,this.over.ty,0.06);
const u=this.mat.uniforms; u.uScrollVelocity.value=vel; u.uMouseEnter.value=this.enter;
u.uMouseOverPos.value.set(this.over.cx,this.over.cy); u.uTime.value=time;
}
}
let prevY=window.scrollY, vel=0;
function resize(){ renderer.setSize(innerWidth,innerHeight); camera.aspect=innerWidth/innerHeight; camera.fov=calcFov(CAMERA_POS); camera.updateProjectionMatrix(); store.forEach(m=>m.measure()); }
addEventListener('resize',resize);
function boot(){
document.querySelectorAll('img[data-gl]').forEach(im=>store.push(new Media(im)));
(function frame(t){ requestAnimationFrame(frame); const time=(t||0)/1000;
const y=window.scrollY; const raw=y-prevY; prevY=y; vel=lerp(vel,raw*0.06,0.1); // smoothed scroll velocity
store.forEach(m=>m.update(y,vel,time)); renderer.render(scene,camera);
})();
}
(function whenReady(cb){ const imgs=[...document.querySelectorAll('img[data-gl]')]; let n=0,t=imgs.length;
const d=()=>{ if(++n>=t) cb(); }; imgs.forEach(im=>{ if(im.complete&&im.naturalWidth) d(); else{ im.onload=d; im.onerror=d; } }); if(!t)cb(); })(boot);
</script>
</body>
</html>
@@ -0,0 +1,109 @@
{
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
"specVersion": "1.0.0",
"name": "example-webgl-distortion-grain",
"title": "Distortion & Grain",
"title_i18n": {
"zh-CN": "失真与颗粒",
"zh-TW": "失真與顆粒",
"ja": "ディストーション&グレイン",
"ko": "디스토션 & 그레인",
"de": "Verzerrung & Korn",
"fr": "Distorsion & grain",
"ru": "Искажение и зерно",
"es": "Distorsión y grano",
"pt-BR": "Distorção e grão",
"en": "Distortion & Grain"
},
"version": "0.1.0",
"description": "A vertical gallery (Three.js) where planes bend with scroll velocity, ripple under the cursor via simplex noise, and carry a film grain.",
"description_i18n": {
"zh-CN": "竖向图廊(Three.js):平面随滚动速度弯曲、在光标处经单纯形噪声起伏失真,并带胶片颗粒。",
"zh-TW": "豎向圖廊(Three.js):平面隨捲動速度彎曲、在游標處經單純形雜訊起伏失真,並帶底片顆粒。",
"en": "A vertical gallery (Three.js) where planes bend with scroll velocity, ripple under the cursor via simplex noise, and carry a film grain."
},
"license": "MIT",
"author": {
"name": "Open Design",
"url": "https://github.com/nexu-io"
},
"credits": [
{
"role": "effect",
"name": "Jan Kohlbach / Codrops",
"license": "MIT"
},
{
"role": "imagery",
"name": "Original (AI-generated)"
},
{
"role": "simplex noise",
"name": "Ashima Arts",
"license": "MIT"
}
],
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/_official/examples/webgl-distortion-grain",
"tags": [
"example",
"first-party",
"gallery",
"webgl",
"three.js",
"scroll",
"distortion",
"grain",
"shader",
"powered-preview",
"untitled"
],
"compat": {
"agentSkills": [
{
"path": "./SKILL.md"
}
]
},
"od": {
"kind": "scenario",
"taskKind": "new-generation",
"mode": "prototype",
"scenario": "design",
"surface": "web",
"preview": {
"type": "html",
"entry": "./example.html"
},
"useCase": {
"query": {
"en": "Build a vertical gallery where image planes bend with scroll velocity, distort under the cursor via simplex noise, and carry a film grain.",
"zh-CN": "构建竖向图廊,图片平面随滚动速度弯曲、在光标处经单纯形噪声失真、并带胶片颗粒。"
}
},
"context": {
"skills": [
{
"path": "./SKILL.md"
}
],
"designSystem": {
"primary": true
}
},
"pipeline": {
"stages": [
{
"id": "generate",
"atoms": [
"file-write",
"live-artifact"
]
}
]
},
"capabilities": [
"prompt:inject",
"fs:write"
]
}
}
@@ -0,0 +1,43 @@
---
name: webgl-halftone-drift
description: |
A self-contained WebGL2 hero: a flowing field screened through a rotated halftone dot grid into a duotone print aesthetic; move the cursor to bend the drift.
triggers:
- "halftone drift"
- "webgl hero"
od:
mode: prototype
platform: web
scenario: design
preview:
type: html
entry: index.html
reload: debounce-100
design_system:
requires: false
craft:
requires: [animation-discipline]
---
# Halftone Drift
Produce a single self-contained `index.html` — A self-contained WebGL2 hero: a flowing field screened through a rotated halftone dot grid into a duotone print aesthetic; move the cursor to bend the drift.
## Why this is a powered artifact
Open Design detects `getContext('webgl2')` / heavy WebGL and renders this file in **powered preview** (a cross-origin-isolated iframe). The full GPU + scroll pipeline runs; no opaque-sandbox workarounds are needed.
## Resource map
```
webgl-halftone-drift/
├── SKILL.md ← you're reading this
├── example.html ← the complete, working artifact (READ FIRST)
└── (assets, if any)
```
## Credits / attribution
- Original, self-contained (no third-party assets).
Keep any bundled LICENSE and on-screen credit intact. Replace imagery only with license-clean assets (original / AI, Lummi.ai, Unsplash/Pexels — never scraped imagery).
@@ -0,0 +1,98 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Halftone Drift — Open Design</title>
<!--
Self-contained WebGL2 hero. A domain-warped flow field is quantised through a
rotated halftone dot screen (variable dot radius by luminance) into a duotone
print aesthetic. The pointer bends the flow. No meshes, no textures, no build.
-->
<style>
:root{ color-scheme:dark; }
html,body{ margin:0; height:100%; overflow:hidden; background:#0b0a12;
font-family:'Albert Sans',ui-sans-serif,system-ui,-apple-system,sans-serif; color:#f6f1ff; }
#gl{ position:fixed; inset:0; width:100%; height:100%; display:block; }
.overlay{ position:fixed; inset:0; z-index:2; pointer-events:none; display:flex; flex-direction:column; justify-content:space-between; padding:5vw; }
.top{ display:flex; justify-content:space-between; font-size:clamp(11px,1vw,14px); letter-spacing:0.32em; text-transform:uppercase; color:rgba(246,241,255,0.72); }
.top .dot{ width:8px; height:8px; border-radius:50%; background:#ff7a45; box-shadow:0 0 14px #ff7a45; display:inline-block; margin-right:10px; vertical-align:middle; }
h1{ margin:0; font-size:clamp(44px,11vw,168px); line-height:0.86; font-weight:800; letter-spacing:-0.04em; mix-blend-mode:difference; }
h1 .thin{ font-weight:300; }
.bottom{ display:flex; justify-content:space-between; align-items:flex-end; gap:4vw; }
.sub{ font-size:clamp(14px,1.5vw,19px); max-width:42ch; color:rgba(246,241,255,0.82); line-height:1.45; margin:0; mix-blend-mode:difference; }
.badge{ font-size:11px; letter-spacing:0.28em; text-transform:uppercase; color:rgba(246,241,255,0.6); border:1px solid rgba(246,241,255,0.22); padding:9px 15px; border-radius:999px; backdrop-filter:blur(8px); white-space:nowrap; }
</style>
</head>
<body>
<canvas id="gl"></canvas>
<div class="overlay">
<div class="top"><span><span class="dot"></span>Open Design · WebGL Preview</span><span>Real-time · GPU</span></div>
<div class="bottom">
<div><h1>HALFTONE<br><span class="thin">DRIFT</span></h1>
<p class="sub">A flowing field screened through a rotated halftone dot grid — print-shop duotone, running live. Move the cursor to bend the drift.</p></div>
<div class="badge">Move the cursor ↗</div>
</div>
</div>
<script>
(() => {
const canvas=document.getElementById('gl');
const gl=canvas.getContext('webgl2',{antialias:true});
if (!gl) { document.body.innerHTML='<p style="color:#fff;padding:2rem;font-family:sans-serif">WebGL2 not available.</p>'; return; }
const VERT=`#version 300 es
void main(){ vec2 p=vec2((gl_VertexID<<1)&2, gl_VertexID&2); gl_Position=vec4(p*2.0-1.0,0.0,1.0); }`;
const FRAG=`#version 300 es
precision highp float;
out vec4 o;
uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse;
mat2 rot(float a){ float c=cos(a),s=sin(a); return mat2(c,-s,s,c); }
float hash(vec2 p){ return fract(sin(dot(p,vec2(41.3,289.1)))*43758.5453); }
float noise(vec2 p){ vec2 i=floor(p),f=fract(p); vec2 u=f*f*(3.0-2.0*f);
return mix(mix(hash(i),hash(i+vec2(1,0)),u.x),mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),u.x),u.y); }
float fbm(vec2 p){ float v=0.0,a=0.55; for(int i=0;i<5;i++){ v+=a*noise(p); p=rot(0.7)*p*1.9+7.0; a*=0.55; } return v; }
// duotone palette (ink → paper with a warm accent)
vec3 duo(float t){
vec3 ink=vec3(0.08,0.06,0.13), paper=vec3(0.98,0.93,0.86), accent=vec3(1.0,0.45,0.22);
vec3 c=mix(ink,accent,smoothstep(0.15,0.6,t));
return mix(c,paper,smoothstep(0.55,1.0,t));
}
void main(){
vec2 uv=(gl_FragCoord.xy-0.5*u_res)/u_res.y;
float t=u_time*0.05;
vec2 m=(u_mouse-0.5)*vec2(u_res.x/u_res.y,1.0);
vec2 pull=(m-uv); float grab=0.3/(dot(pull,pull)+0.3);
// flow field
vec2 q=vec2(fbm(uv*1.3+vec2(0.0,t)), fbm(uv*1.3+vec2(4.0,-t)));
float v=fbm(uv*1.3+2.4*q + grab*pull + t);
v=smoothstep(0.15,0.95,v);
// rotated halftone screen: dot radius grows with value
float cells=u_res.y/9.0;
vec2 sc=rot(0.4)*(gl_FragCoord.xy/u_res.y)*cells;
vec2 g=fract(sc)-0.5;
float dotr=sqrt(v)*0.72;
float dotm=smoothstep(dotr, dotr-0.09, length(g)); // 1 inside dot
vec3 col=mix(duo(v)*0.35, duo(v), dotm);
col+=(hash(gl_FragCoord.xy+u_time)-0.5)*0.02;
col*=0.7+0.5*smoothstep(1.4,0.1,length(uv));
o=vec4(pow(max(col,0.0),vec3(0.9)),1.0);
}`;
function sh(t,s){const x=gl.createShader(t);gl.shaderSource(x,s);gl.compileShader(x);if(!gl.getShaderParameter(x,gl.COMPILE_STATUS))throw gl.getShaderInfoLog(x);return x;}
const pr=gl.createProgram();gl.attachShader(pr,sh(gl.VERTEX_SHADER,VERT));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,FRAG));gl.linkProgram(pr);
if(!gl.getProgramParameter(pr,gl.LINK_STATUS))throw gl.getProgramInfoLog(pr);
gl.useProgram(pr);
const uRes=gl.getUniformLocation(pr,'u_res'),uTime=gl.getUniformLocation(pr,'u_time'),uMouse=gl.getUniformLocation(pr,'u_mouse');
let mouse=[0.5,0.5],target=[0.5,0.5];
addEventListener('pointermove',e=>{target=[e.clientX/innerWidth,1.0-e.clientY/innerHeight];});
function resize(){const d=Math.min(devicePixelRatio||1,2);canvas.width=innerWidth*d|0;canvas.height=innerHeight*d|0;gl.viewport(0,0,canvas.width,canvas.height);}
addEventListener('resize',resize);resize();
const t0=performance.now();
function frame(now){ mouse[0]+=(target[0]-mouse[0])*0.06; mouse[1]+=(target[1]-mouse[1])*0.06;
gl.uniform2f(uRes,canvas.width,canvas.height); gl.uniform1f(uTime,(now-t0)/1000); gl.uniform2f(uMouse,mouse[0],mouse[1]);
gl.drawArrays(gl.TRIANGLES,0,3); requestAnimationFrame(frame); }
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
@@ -0,0 +1,93 @@
{
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
"specVersion": "1.0.0",
"name": "example-webgl-halftone-drift",
"title": "Halftone Drift",
"title_i18n": {
"zh-CN": "半调漂移",
"zh-TW": "半調漂移",
"ja": "ハーフトーン・ドリフト",
"ko": "하프톤 드리프트",
"de": "Halbton-Drift",
"fr": "Dérive en trame",
"ru": "Полутоновый дрейф",
"es": "Deriva de semitono",
"pt-BR": "Deriva de meio-tom",
"en": "Halftone Drift"
},
"version": "0.1.0",
"description": "A self-contained WebGL2 hero: a flowing field screened through a rotated halftone dot grid into a duotone print aesthetic; move the cursor to bend the drift.",
"description_i18n": {
"zh-CN": "自包含 WebGL2 主视觉:流场经旋转半调网点屏幕化为双色印刷质感;移动光标扰动漂移。",
"zh-TW": "自包含 WebGL2 主視覺:流場經旋轉半調網點螢幕化為雙色印刷質感;移動游標擾動漂移。",
"en": "A self-contained WebGL2 hero: a flowing field screened through a rotated halftone dot grid into a duotone print aesthetic; move the cursor to bend the drift."
},
"license": "MIT",
"author": {
"name": "Open Design",
"url": "https://github.com/nexu-io"
},
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/_official/examples/webgl-halftone-drift",
"tags": [
"example",
"first-party",
"hero",
"webgl",
"webgl2",
"shader",
"halftone",
"duotone",
"generative",
"powered-preview",
"untitled"
],
"compat": {
"agentSkills": [
{
"path": "./SKILL.md"
}
]
},
"od": {
"kind": "scenario",
"taskKind": "new-generation",
"mode": "prototype",
"scenario": "design",
"surface": "web",
"preview": {
"type": "html",
"entry": "./example.html"
},
"useCase": {
"query": {
"en": "Build a self-contained WebGL2 hero of a flowing field screened through a rotated halftone dot grid into a duotone print look, cursor-reactive.",
"zh-CN": "构建自包含 WebGL2 主视觉:流场经旋转半调网点屏幕化的双色印刷质感,光标可扰动。"
}
},
"context": {
"skills": [
{
"path": "./SKILL.md"
}
],
"designSystem": {
"primary": true
}
},
"pipeline": {
"stages": [
{
"id": "generate",
"atoms": [
"file-write",
"live-artifact"
]
}
]
},
"capabilities": [
"prompt:inject",
"fs:write"
]
}
}
@@ -0,0 +1,43 @@
---
name: webgl-holographic-foil
description: |
A self-contained WebGL2 hero: thin-film interference over a crushed-foil surface whose palette shifts with the viewing angle; move the cursor to tilt the film.
triggers:
- "holographic foil"
- "webgl hero"
od:
mode: prototype
platform: web
scenario: design
preview:
type: html
entry: index.html
reload: debounce-100
design_system:
requires: false
craft:
requires: [animation-discipline]
---
# Holographic Foil
Produce a single self-contained `index.html` — A self-contained WebGL2 hero: thin-film interference over a crushed-foil surface whose palette shifts with the viewing angle; move the cursor to tilt the film.
## Why this is a powered artifact
Open Design detects `getContext('webgl2')` / heavy WebGL and renders this file in **powered preview** (a cross-origin-isolated iframe). The full GPU + scroll pipeline runs; no opaque-sandbox workarounds are needed.
## Resource map
```
webgl-holographic-foil/
├── SKILL.md ← you're reading this
├── example.html ← the complete, working artifact (READ FIRST)
└── (assets, if any)
```
## Credits / attribution
- Original, self-contained (no third-party assets).
Keep any bundled LICENSE and on-screen credit intact. Replace imagery only with license-clean assets (original / AI, Lummi.ai, Unsplash/Pexels — never scraped imagery).
@@ -0,0 +1,96 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Holographic Foil — Open Design</title>
<!--
Self-contained WebGL2 hero. Thin-film interference: a warped "thickness" field
drives a cosine interference palette that shifts with view angle (the pointer
tilts the film), plus a crushed-foil specular. No meshes, no textures, no build.
-->
<style>
:root{ color-scheme:dark; }
html,body{ margin:0; height:100%; overflow:hidden; background:#050509;
font-family:'Albert Sans',ui-sans-serif,system-ui,-apple-system,sans-serif; color:#ffffff; }
#gl{ position:fixed; inset:0; width:100%; height:100%; display:block; }
.overlay{ position:fixed; inset:0; z-index:2; pointer-events:none; display:flex; flex-direction:column; justify-content:space-between; padding:5vw; }
.top{ display:flex; justify-content:space-between; font-size:clamp(11px,1vw,14px); letter-spacing:0.32em; text-transform:uppercase; color:rgba(255,255,255,0.75); }
.top .dot{ width:8px; height:8px; border-radius:50%; background:#fff; box-shadow:0 0 14px #fff; display:inline-block; margin-right:10px; vertical-align:middle; }
h1{ margin:0; font-size:clamp(44px,11vw,168px); line-height:0.86; font-weight:800; letter-spacing:-0.04em; mix-blend-mode:overlay; }
h1 .thin{ font-weight:300; }
.bottom{ display:flex; justify-content:space-between; align-items:flex-end; gap:4vw; }
.sub{ font-size:clamp(14px,1.5vw,19px); max-width:42ch; color:rgba(255,255,255,0.85); line-height:1.45; margin:0; }
.badge{ font-size:11px; letter-spacing:0.28em; text-transform:uppercase; color:rgba(255,255,255,0.65); border:1px solid rgba(255,255,255,0.25); padding:9px 15px; border-radius:999px; backdrop-filter:blur(8px); white-space:nowrap; }
</style>
</head>
<body>
<canvas id="gl"></canvas>
<div class="overlay">
<div class="top"><span><span class="dot"></span>Open Design · WebGL Preview</span><span>Real-time · GPU</span></div>
<div class="bottom">
<div><h1>HOLOGRAPHIC<br><span class="thin">FOIL</span></h1>
<p class="sub">Thin-film interference over a crushed-foil surface — the palette shifts with the viewing angle. Move the cursor to tilt the film.</p></div>
<div class="badge">Tilt the film ↗</div>
</div>
</div>
<script>
(() => {
const canvas=document.getElementById('gl');
const gl=canvas.getContext('webgl2',{antialias:true});
if (!gl) { document.body.innerHTML='<p style="color:#fff;padding:2rem;font-family:sans-serif">WebGL2 not available.</p>'; return; }
const VERT=`#version 300 es
void main(){ vec2 p=vec2((gl_VertexID<<1)&2, gl_VertexID&2); gl_Position=vec4(p*2.0-1.0,0.0,1.0); }`;
const FRAG=`#version 300 es
precision highp float;
out vec4 o;
uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse;
mat2 rot(float a){ float c=cos(a),s=sin(a); return mat2(c,-s,s,c); }
float hash(vec2 p){ return fract(sin(dot(p,vec2(41.3,289.1)))*43758.5453); }
float noise(vec2 p){ vec2 i=floor(p),f=fract(p); vec2 u=f*f*(3.0-2.0*f);
return mix(mix(hash(i),hash(i+vec2(1,0)),u.x),mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),u.x),u.y); }
float fbm(vec2 p){ float v=0.0,a=0.55; for(int i=0;i<6;i++){ v+=a*noise(p); p=rot(0.6)*p*1.95+9.0; a*=0.55; } return v; }
// interference palette: thickness+angle → spectral color
vec3 iri(float t){ return 0.55+0.45*cos(6.28318*(vec3(1.0,1.0,1.0)*t + vec3(0.0,0.33,0.67))); }
void main(){
vec2 uv=(gl_FragCoord.xy-0.5*u_res)/u_res.y;
float t=u_time*0.05;
vec2 tilt=(u_mouse-0.5)*1.4; // view-angle proxy
// crushed-foil thickness field (domain-warped)
vec2 q=vec2(fbm(uv*2.0+vec2(0.0,t)), fbm(uv*2.0+vec2(3.3,-t)));
float thick=fbm(uv*2.2 + 2.5*q + t*0.5);
// faceted foil normal from thickness gradient
float e=0.02;
float gx=fbm((uv+vec2(e,0))*2.2+2.5*q)-fbm((uv-vec2(e,0))*2.2+2.5*q);
float gy=fbm((uv+vec2(0,e))*2.2+2.5*q)-fbm((uv-vec2(0,e))*2.2+2.5*q);
vec2 n=vec2(gx,gy)/(2.0*e);
// interference phase driven by thickness + view angle (tilt·normal)
float phase=thick*3.0 + dot(n,tilt)*0.6 + 0.15;
vec3 col=iri(phase);
col=mix(col*0.4, col, smoothstep(0.2,0.9,thick)); // deepen thin areas
// crushed specular glints along facet ridges
float ridge=pow(clamp(1.0-abs(dot(normalize(n+1e-4), normalize(tilt+1e-4))),0.0,1.0),8.0);
col+=vec3(1.0)*pow(fbm(uv*8.0+q*3.0),6.0)*0.5; // sparkle
col+=vec3(0.9,0.95,1.0)*ridge*0.4;
col*=0.7+0.5*smoothstep(1.4,0.05,length(uv));
col=col/(col+0.7);
o=vec4(pow(max(col,0.0),vec3(0.85)),1.0);
}`;
function sh(t,s){const x=gl.createShader(t);gl.shaderSource(x,s);gl.compileShader(x);if(!gl.getShaderParameter(x,gl.COMPILE_STATUS))throw gl.getShaderInfoLog(x);return x;}
const pr=gl.createProgram();gl.attachShader(pr,sh(gl.VERTEX_SHADER,VERT));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,FRAG));gl.linkProgram(pr);
if(!gl.getProgramParameter(pr,gl.LINK_STATUS))throw gl.getProgramInfoLog(pr);
gl.useProgram(pr);
const uRes=gl.getUniformLocation(pr,'u_res'),uTime=gl.getUniformLocation(pr,'u_time'),uMouse=gl.getUniformLocation(pr,'u_mouse');
let mouse=[0.5,0.5],target=[0.5,0.5];
addEventListener('pointermove',e=>{target=[e.clientX/innerWidth,1.0-e.clientY/innerHeight];});
function resize(){const d=Math.min(devicePixelRatio||1,2);canvas.width=innerWidth*d|0;canvas.height=innerHeight*d|0;gl.viewport(0,0,canvas.width,canvas.height);}
addEventListener('resize',resize);resize();
const t0=performance.now();
function frame(now){ mouse[0]+=(target[0]-mouse[0])*0.05; mouse[1]+=(target[1]-mouse[1])*0.05;
gl.uniform2f(uRes,canvas.width,canvas.height); gl.uniform1f(uTime,(now-t0)/1000); gl.uniform2f(uMouse,mouse[0],mouse[1]);
gl.drawArrays(gl.TRIANGLES,0,3); requestAnimationFrame(frame); }
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
@@ -0,0 +1,93 @@
{
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
"specVersion": "1.0.0",
"name": "example-webgl-holographic-foil",
"title": "Holographic Foil",
"title_i18n": {
"zh-CN": "全息箔片",
"zh-TW": "全息箔片",
"ja": "ホログラフィック・フォイル",
"ko": "홀로그래픽 포일",
"de": "Holografische Folie",
"fr": "Feuille holographique",
"ru": "Голографическая фольга",
"es": "Lámina holográfica",
"pt-BR": "Folha holográfica",
"en": "Holographic Foil"
},
"version": "0.1.0",
"description": "A self-contained WebGL2 hero: thin-film interference over a crushed-foil surface whose palette shifts with the viewing angle; move the cursor to tilt the film.",
"description_i18n": {
"zh-CN": "自包含 WebGL2 主视觉:褶皱箔片表面的薄膜干涉,调色随视角变化;移动光标倾斜箔片。",
"zh-TW": "自包含 WebGL2 主視覺:褶皺箔片表面的薄膜干涉,調色隨視角變化;移動游標傾斜箔片。",
"en": "A self-contained WebGL2 hero: thin-film interference over a crushed-foil surface whose palette shifts with the viewing angle; move the cursor to tilt the film."
},
"license": "MIT",
"author": {
"name": "Open Design",
"url": "https://github.com/nexu-io"
},
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/_official/examples/webgl-holographic-foil",
"tags": [
"example",
"first-party",
"hero",
"webgl",
"webgl2",
"shader",
"iridescent",
"foil",
"generative",
"powered-preview",
"untitled"
],
"compat": {
"agentSkills": [
{
"path": "./SKILL.md"
}
]
},
"od": {
"kind": "scenario",
"taskKind": "new-generation",
"mode": "prototype",
"scenario": "design",
"surface": "web",
"preview": {
"type": "html",
"entry": "./example.html"
},
"useCase": {
"query": {
"en": "Build a self-contained WebGL2 hero of thin-film interference over a crushed foil, palette shifting with viewing angle, cursor-tilted.",
"zh-CN": "构建自包含 WebGL2 主视觉:褶皱箔片薄膜干涉,调色随视角变化,光标可倾斜。"
}
},
"context": {
"skills": [
{
"path": "./SKILL.md"
}
],
"designSystem": {
"primary": true
}
},
"pipeline": {
"stages": [
{
"id": "generate",
"atoms": [
"file-write",
"live-artifact"
]
}
]
},
"capabilities": [
"prompt:inject",
"fs:write"
]
}
}
@@ -0,0 +1,23 @@
MIT License
Effect © David Faure / Codrops. Ported to a single self-contained file and packaged for Open Design.
Original: https://github.com/davidfaure/horizontal-parallax-gallery-codrops (declared MIT).
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Sample imagery is original (AI-generated).
@@ -0,0 +1,44 @@
---
name: webgl-horizontal-parallax
description: |
A horizontal-scroll WebGL gallery (Three.js): frames glide sideways with lerp smoothing and each image parallaxes its texture (UV shift) by its position in the viewport.
triggers:
- "horizontal parallax gallery"
- "webgl gallery"
od:
mode: prototype
platform: web
scenario: design
preview:
type: html
entry: index.html
reload: debounce-100
design_system:
requires: false
craft:
requires: [animation-discipline]
---
# Horizontal Parallax Gallery
Produce a single self-contained `index.html` — A horizontal-scroll WebGL gallery (Three.js): frames glide sideways with lerp smoothing and each image parallaxes its texture (UV shift) by its position in the viewport.
## Why this is a powered artifact
Open Design detects `getContext('webgl2')` / heavy WebGL and renders this file in **powered preview** (a cross-origin-isolated iframe). The full GPU + scroll pipeline runs; no opaque-sandbox workarounds are needed.
## Resource map
```
webgl-horizontal-parallax/
├── SKILL.md ← you're reading this
├── example.html ← the complete, working artifact (READ FIRST)
└── (assets, if any)
```
## Credits / attribution
- effect: David Faure / Codrops (MIT)
- imagery: Original (AI-generated)
Keep any bundled LICENSE and on-screen credit intact. Replace imagery only with license-clean assets (original / AI, Lummi.ai, Unsplash/Pexels — never scraped imagery).
@@ -0,0 +1,137 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Horizontal Parallax — Open Design</title>
<!--
Faithful single-file port of davidfaure's "Horizontal Parallax Gallery"
(Codrops, MIT). Original: https://github.com/davidfaure/horizontal-parallax-gallery-codrops
WebGL planes are synced to a horizontal DOM row; wheel scrolls it sideways with
lerp smoothing, and each plane parallaxes its texture (UV shift) by its position
in the viewport. Three.js via CDN; images original (generated, not the author's
Midjourney set). Runs in powered preview.
-->
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Manrope:wght@700;800&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box} :root{ --bg:#0c0c0e; --ink:#ededf0; --mut:#8b8b93; }
html,body{ margin:0; height:100%; overflow:hidden; background:var(--bg); color:var(--ink);
font-family:'Manrope',system-ui,sans-serif; -webkit-font-smoothing:antialiased; }
#gl{ position:fixed; inset:0; width:100%; height:100%; z-index:2; pointer-events:none; }
.frame{ position:fixed; inset:0; z-index:5; pointer-events:none; padding:1.4rem 1.6rem;
display:flex; justify-content:space-between; align-items:flex-start;
font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.22em; text-transform:uppercase; color:var(--mut); }
.frame .dot{ width:7px;height:7px;border-radius:50%;background:#b8ff3c;box-shadow:0 0 12px #b8ff3c;display:inline-block;margin-right:8px;vertical-align:middle;}
.frame a{ color:var(--mut); pointer-events:auto; }
.title{ position:fixed; left:1.6rem; top:44%; transform:translateY(-50%); z-index:4; pointer-events:none; mix-blend-mode:difference; }
.title h1{ margin:0; font-size:clamp(40px,7vw,96px); font-weight:800; line-height:0.92; letter-spacing:-0.03em; }
.title p{ margin:0.8rem 0 0; max-width:30ch; font-family:'IBM Plex Mono',monospace; font-size:12px; color:var(--ink); line-height:1.5; }
/* horizontal DOM row (provides layout/bounds; WebGL draws the images) */
#wrap{ position:fixed; inset:0; z-index:1; overflow:hidden; }
#row{ position:absolute; top:50%; transform:translateY(-50%); left:0; display:flex; align-items:center; gap:5vw; padding:0 46vw 0 6vw; will-change:transform; }
.cell{ flex:0 0 auto; text-decoration:none; color:inherit; }
.cell .img{ display:block; height:64vh; opacity:0; } /* WebGL plane covers it */
.cell .cap{ margin-top:0.8rem; font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.06em; text-transform:uppercase;
color:var(--mut); display:flex; justify-content:space-between; gap:2rem; }
.hint{ position:fixed; right:1.6rem; bottom:1.4rem; z-index:5; font-family:'IBM Plex Mono',monospace; font-size:11px;
letter-spacing:0.22em; text-transform:uppercase; color:var(--mut); }
</style>
</head>
<body>
<canvas id="gl"></canvas>
<div class="frame"><span><span class="dot"></span>Open Design · WebGL Preview</span>
<span>Effect: <a href="https://github.com/davidfaure/horizontal-parallax-gallery-codrops" target="_blank">David Faure / Codrops</a></span></div>
<div class="title"><h1>Drift<br>Sideways</h1><p>Scroll — the frames glide sideways and each image parallaxes inside its own window.</p></div>
<div id="wrap"><div id="row"></div></div>
<div class="hint">Scroll → horizontal</div>
<script>
const DATA = [
{ src:'hpg-img/1.jpg', w:0.30, name:'Monolith', idx:'01' },
{ src:'hpg-img/2.jpg', w:0.24, name:'Cantilever', idx:'02' },
{ src:'hpg-img/3.jpg', w:0.34, name:'Sky Well', idx:'03' },
{ src:'hpg-img/4.jpg', w:0.26, name:'Buttress', idx:'04' },
{ src:'hpg-img/5.jpg', w:0.32, name:'Concourse', idx:'05' },
{ src:'hpg-img/6.jpg', w:0.28, name:'Overhang', idx:'06' },
];
const row=document.getElementById('row');
DATA.forEach(d=>{ const a=document.createElement('a'); a.className='cell'; a.href='#';
a.innerHTML=`<img class="img gl" src="${d.src}" crossorigin="anonymous" style="width:${Math.round(d.w*innerWidth)}px">
<div class="cap"><span>${d.idx} · ${d.name}</span><span>◍</span></div>`;
row.appendChild(a); });
</script>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
const VERT=`varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }`;
const FRAG=`precision highp float; varying vec2 vUv;
uniform sampler2D uTexture; uniform vec2 uResolution; uniform vec2 uImageResolution; uniform float uParallax; uniform float uUvScale;
vec2 coverUv(vec2 uv, vec2 res, vec2 img){
vec2 r=vec2(min((res.x/res.y)/(img.x/img.y),1.0), min((res.y/res.x)/(img.y/img.x),1.0));
return vec2(uv.x*r.x+(1.0-r.x)*0.5, uv.y*r.y+(1.0-r.y)*0.5);
}
void main(){
vec2 uv=coverUv(vUv,uResolution,uImageResolution);
uv.x += uParallax; // horizontal parallax
uv=(uv-0.5)*uUvScale+0.5; // buffer room
gl_FragColor=vec4(texture2D(uTexture,uv).rgb,1.0);
}`;
const canvas=document.getElementById('gl');
const renderer=new THREE.WebGLRenderer({canvas,antialias:true,alpha:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
const scene=new THREE.Scene(); const group=new THREE.Group(); scene.add(group);
let W=innerWidth,H=innerHeight;
const fov=()=>2*Math.atan(H/2/100)*(180/Math.PI);
const camera=new THREE.PerspectiveCamera(fov(),W/H,0.01,1000); camera.position.z=100;
const geo=new THREE.PlaneGeometry(1,1,32,32);
const loader=new THREE.TextureLoader();
const medias=[];
class Media{
constructor(el){ this.el=el; this.parallaxIntensity=0.4;
this.mat=new THREE.ShaderMaterial({vertexShader:VERT,fragmentShader:FRAG,uniforms:{
uTexture:{value:null}, uResolution:{value:new THREE.Vector2(1,1)}, uImageResolution:{value:new THREE.Vector2(1,1)},
uParallax:{value:0}, uUvScale:{value:0.85} }});
this.mesh=new THREE.Mesh(geo,this.mat); group.add(this.mesh);
this.mat.uniforms.uTexture.value=loader.load(el.src,t=>{ t.colorSpace=THREE.SRGBColorSpace;
this.mat.uniforms.uImageResolution.value.set(t.image.width,t.image.height); });
this.measure();
}
measure(){ this.b=this.el.getBoundingClientRect(); // captured at scroll 0
this.mesh.scale.set(this.b.width,this.b.height,1);
this.mat.uniforms.uResolution.value.set(this.b.width,this.b.height); }
render(scroll){
const x=this.b.left - scroll - W/2 + this.b.width/2;
const y=-this.b.top + H/2 - this.b.height/2;
this.mesh.position.set(x,y,0);
const center=this.b.left - scroll + this.b.width/2;
const dist=(center - W/2)/W;
this.mat.uniforms.uParallax.value=dist*this.parallaxIntensity;
}
}
// scroll state
const scroll={current:0,target:0,ease:0.07,limit:0};
const wrap=document.getElementById('wrap');
function setLimit(){ scroll.limit=Math.max(0,row.scrollWidth - wrap.clientWidth); }
addEventListener('wheel',e=>{ scroll.target+=e.deltaY; },{passive:true});
function resize(){ W=innerWidth;H=innerHeight; camera.aspect=W/H; camera.fov=fov(); camera.updateProjectionMatrix();
renderer.setSize(W,H); renderer.setPixelRatio(Math.min(devicePixelRatio,2));
document.querySelectorAll('.cell .img').forEach((im,i)=>{ im.style.width=Math.round(DATA[i].w*W)+'px'; });
setLimit(); medias.forEach(m=>m.measure()); }
function boot(){
document.querySelectorAll('.cell .img').forEach(im=>medias.push(new Media(im)));
setLimit();
addEventListener('resize',resize);
(function frame(){ requestAnimationFrame(frame);
scroll.target=Math.max(0,Math.min(scroll.limit,scroll.target));
scroll.current+=(scroll.target-scroll.current)*scroll.ease;
row.style.transform=`translateY(-50%) translateX(${-scroll.current}px)`;
medias.forEach(m=>m.render(scroll.current));
renderer.render(scene,camera);
})();
}
// wait images so bounds/aspect are correct
(function whenReady(cb){ const imgs=[...document.querySelectorAll('.cell .img')]; let n=0,t=imgs.length;
const d=()=>{ if(++n>=t) cb(); }; imgs.forEach(im=>{ if(im.complete&&im.naturalWidth) d(); else{ im.onload=d; im.onerror=d; } }); if(!t)cb(); })(boot);
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

@@ -0,0 +1,103 @@
{
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
"specVersion": "1.0.0",
"name": "example-webgl-horizontal-parallax",
"title": "Horizontal Parallax Gallery",
"title_i18n": {
"zh-CN": "横向视差画廊",
"zh-TW": "橫向視差畫廊",
"ja": "横スクロール視差ギャラリー",
"ko": "가로 패럴랙스 갤러리",
"de": "Horizontale Parallax-Galerie",
"fr": "Galerie parallaxe horizontale",
"ru": "Горизонтальная параллакс-галерея",
"es": "Galería de paralaje horizontal",
"pt-BR": "Galeria de paralaxe horizontal",
"en": "Horizontal Parallax Gallery"
},
"version": "0.1.0",
"description": "A horizontal-scroll WebGL gallery (Three.js): frames glide sideways with lerp smoothing and each image parallaxes its texture (UV shift) by its position in the viewport.",
"description_i18n": {
"zh-CN": "横向滚动 WebGL 画廊(Three.js):画框随 lerp 平滑横移,每张图按其在视口的位置做纹理视差(UV 位移)。",
"zh-TW": "橫向捲動 WebGL 畫廊(Three.js):畫框隨 lerp 平滑橫移,每張圖按其在視口的位置做紋理視差(UV 位移)。",
"en": "A horizontal-scroll WebGL gallery (Three.js): frames glide sideways with lerp smoothing and each image parallaxes its texture (UV shift) by its position in the viewport."
},
"license": "MIT",
"author": {
"name": "Open Design",
"url": "https://github.com/nexu-io"
},
"credits": [
{
"role": "effect",
"name": "David Faure / Codrops",
"license": "MIT"
},
{
"role": "imagery",
"name": "Original (AI-generated)"
}
],
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/_official/examples/webgl-horizontal-parallax",
"tags": [
"example",
"first-party",
"gallery",
"webgl",
"three.js",
"scroll",
"parallax",
"horizontal",
"powered-preview",
"untitled"
],
"compat": {
"agentSkills": [
{
"path": "./SKILL.md"
}
]
},
"od": {
"kind": "scenario",
"taskKind": "new-generation",
"mode": "prototype",
"scenario": "design",
"surface": "web",
"preview": {
"type": "html",
"entry": "./example.html"
},
"useCase": {
"query": {
"en": "Build a horizontal-scroll WebGL gallery where frames glide sideways with smoothing and each image parallaxes its texture by its viewport position.",
"zh-CN": "构建横向滚动 WebGL 画廊,画框平滑横移,每图按视口位置做纹理视差。"
}
},
"context": {
"skills": [
{
"path": "./SKILL.md"
}
],
"designSystem": {
"primary": true
}
},
"pipeline": {
"stages": [
{
"id": "generate",
"atoms": [
"file-write",
"live-artifact"
]
}
]
},
"capabilities": [
"prompt:inject",
"fs:write"
]
}
}
@@ -0,0 +1,43 @@
---
name: webgl-liquid-iridescence
description: |
A self-contained WebGL2 hero: a living oil-on-water field brightened into iridescent caustic filaments; move the cursor to bend the flow.
triggers:
- "liquid iridescence"
- "webgl hero"
od:
mode: prototype
platform: web
scenario: design
preview:
type: html
entry: index.html
reload: debounce-100
design_system:
requires: false
craft:
requires: [animation-discipline]
---
# Liquid Iridescence
Produce a single self-contained `index.html` — A self-contained WebGL2 hero: a living oil-on-water field brightened into iridescent caustic filaments; move the cursor to bend the flow.
## Why this is a powered artifact
Open Design detects `getContext('webgl2')` / heavy WebGL and renders this file in **powered preview** (a cross-origin-isolated iframe). The full GPU + scroll pipeline runs; no opaque-sandbox workarounds are needed.
## Resource map
```
webgl-liquid-iridescence/
├── SKILL.md ← you're reading this
├── example.html ← the complete, working artifact (READ FIRST)
└── (assets, if any)
```
## Credits / attribution
- Original, self-contained (no third-party assets).
Keep any bundled LICENSE and on-screen credit intact. Replace imagery only with license-clean assets (original / AI, Lummi.ai, Unsplash/Pexels — never scraped imagery).
@@ -0,0 +1,167 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Liquid Iridescence — Open Design</title>
<!--
Self-contained WebGL2 hero. A single fullscreen triangle runs a domain-warped
FBM flow field, colored with an iridescent cosine palette and sharpened into
oil-on-water caustic ridges. Pointer position warps the flow (interactive),
a soft chromatic split gives the "liquid glass" sheen, and a hashed grain
breaks up banding. No meshes, no textures, no build step, no external assets.
-->
<style>
:root { color-scheme: dark; }
html, body { margin: 0; height: 100%; overflow: hidden; background: #05060a;
font-family: 'Albert Sans', ui-sans-serif, system-ui, -apple-system, sans-serif; color: #f5f3ff; }
#gl { position: fixed; inset: 0; width: 100%; height: 100%; display: block; }
.overlay { position: fixed; inset: 0; z-index: 2; pointer-events: none;
display: flex; flex-direction: column; justify-content: space-between; padding: 5vw; }
.top { display: flex; justify-content: space-between; align-items: center;
font-size: clamp(11px, 1vw, 14px); letter-spacing: 0.32em; text-transform: uppercase;
color: rgba(245,243,255,0.72); }
.top .dot { width: 8px; height: 8px; border-radius: 50%; background: #b8ff3c;
box-shadow: 0 0 14px #b8ff3c; display: inline-block; margin-right: 10px; vertical-align: middle; }
h1 { margin: 0; font-size: clamp(44px, 11vw, 168px); line-height: 0.86; font-weight: 800;
letter-spacing: -0.04em; mix-blend-mode: overlay; text-shadow: 0 4px 60px rgba(0,0,0,0.35); }
h1 .thin { font-weight: 300; letter-spacing: -0.02em; }
.bottom { display: flex; justify-content: space-between; align-items: flex-end; gap: 4vw; }
.sub { font-size: clamp(14px, 1.5vw, 19px); max-width: 42ch; color: rgba(245,243,255,0.78);
line-height: 1.45; margin: 0; }
.badge { font-size: 11px; letter-spacing: 0.28em; text-transform: uppercase;
color: rgba(245,243,255,0.6); border: 1px solid rgba(245,243,255,0.22);
padding: 9px 15px; border-radius: 999px; backdrop-filter: blur(8px); white-space: nowrap; }
</style>
</head>
<body>
<canvas id="gl"></canvas>
<div class="overlay">
<div class="top">
<span><span class="dot"></span>Open Design · WebGL Preview</span>
<span>Real-time · GPU</span>
</div>
<div class="bottom">
<div>
<h1>LIQUID<br><span class="thin">IRIDESCENCE</span></h1>
<p class="sub">A living oil-on-water field, sphere-free and texture-free — every ripple is math, running live in the preview. Move your cursor to bend the flow.</p>
</div>
<div class="badge">Move the cursor ↗</div>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('gl');
const gl = canvas.getContext('webgl2', { antialias: true, premultipliedAlpha: false });
if (!gl) { document.body.innerHTML = '<p style="color:#fff;padding:2rem;font-family:sans-serif">WebGL2 not available.</p>'; return; }
const VERT = `#version 300 es
void main(){
vec2 p = vec2((gl_VertexID<<1)&2, gl_VertexID&2);
gl_Position = vec4(p*2.0-1.0, 0.0, 1.0);
}`;
const FRAG = `#version 300 es
precision highp float;
out vec4 outColor;
uniform vec2 u_res;
uniform float u_time;
uniform vec2 u_mouse; // 0..1, smoothed
// iq cosine palette
vec3 pal(float t){
return 0.5 + 0.5*cos(6.28318*(vec3(1.0)*t + vec3(0.00,0.33,0.67)));
}
mat2 rot(float a){ float c=cos(a), s=sin(a); return mat2(c,-s,s,c); }
float hash(vec2 p){ return fract(sin(dot(p,vec2(41.3,289.1)))*43758.5453); }
float noise(vec2 p){
vec2 i=floor(p), f=fract(p);
vec2 u=f*f*(3.0-2.0*f);
float a=hash(i), b=hash(i+vec2(1,0)), c=hash(i+vec2(0,1)), d=hash(i+vec2(1,1));
return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);
}
float fbm(vec2 p){
float v=0.0, amp=0.55;
for(int i=0;i<6;i++){ v+=amp*noise(p); p=rot(0.6)*p*1.9+11.0; amp*=0.55; }
return v;
}
void main(){
vec2 uv = (gl_FragCoord.xy - 0.5*u_res) / u_res.y;
float t = u_time*0.06;
// pointer pulls the field toward the cursor
vec2 m = (u_mouse - 0.5) * vec2(u_res.x/u_res.y, 1.0);
vec2 pull = (m - uv);
float grab = 0.35/(dot(pull,pull)+0.35);
// domain warp: two chained flow layers
vec2 q = vec2( fbm(uv*1.6 + vec2(0.0, t)),
fbm(uv*1.6 + vec2(5.2,-t)) );
vec2 r = vec2( fbm(uv*1.6 + 3.2*q + vec2(1.7, 9.2) + grab*pull),
fbm(uv*1.6 + 3.2*q + vec2(8.3, 2.8) - grab*pull) );
float f = fbm(uv*1.6 + 4.0*r + t);
// oil-on-water caustic ridges
float ridge = abs(fract(f*3.0 + length(r) + t*2.0)*2.0 - 1.0);
ridge = pow(1.0 - ridge, 2.5);
// iridescent color driven by flow + a subtle chromatic split for glass sheen
float hue = f + length(r)*0.5 + 0.15*u_time*0.02;
vec3 col = pal(hue);
col += pal(hue+0.04)*0.35; // chroma bleed
col = mix(col*0.35, col, ridge); // ridges glow, valleys deepen
col += vec3(0.9,0.95,1.0)*pow(ridge,3.0)*0.6; // specular caustic highlight
// depth: darken toward edges, lift center
float vig = smoothstep(1.25, 0.15, length(uv));
col *= 0.55 + 0.75*vig;
// filmic-ish tone + grain
col = col/(col+0.6);
col += (hash(gl_FragCoord.xy + u_time) - 0.5)*0.03;
outColor = vec4(pow(max(col,0.0), vec3(0.85)), 1.0);
}`;
function compile(type, src){
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
return s;
}
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog));
gl.useProgram(prog);
const uRes = gl.getUniformLocation(prog, 'u_res');
const uTime = gl.getUniformLocation(prog, 'u_time');
const uMouse = gl.getUniformLocation(prog, 'u_mouse');
let mouse = [0.5, 0.5], target = [0.5, 0.5];
addEventListener('pointermove', e => { target = [e.clientX/innerWidth, 1.0 - e.clientY/innerHeight]; });
function resize(){
const dpr = Math.min(devicePixelRatio || 1, 2);
canvas.width = Math.floor(innerWidth*dpr);
canvas.height = Math.floor(innerHeight*dpr);
gl.viewport(0, 0, canvas.width, canvas.height);
}
addEventListener('resize', resize); resize();
const start = performance.now();
function frame(now){
mouse[0] += (target[0]-mouse[0])*0.06;
mouse[1] += (target[1]-mouse[1])*0.06;
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform1f(uTime, (now-start)/1000);
gl.uniform2f(uMouse, mouse[0], mouse[1]);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
@@ -0,0 +1,93 @@
{
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
"specVersion": "1.0.0",
"name": "example-webgl-liquid-iridescence",
"title": "Liquid Iridescence",
"title_i18n": {
"zh-CN": "虹彩流体",
"zh-TW": "虹彩流體",
"ja": "リキッド・イリデッセンス",
"ko": "리퀴드 이리데센스",
"de": "Flüssige Iridiszenz",
"fr": "Iridescence liquide",
"ru": "Жидкая иридесценция",
"es": "Iridiscencia líquida",
"pt-BR": "Iridescência líquida",
"en": "Liquid Iridescence"
},
"version": "0.1.0",
"description": "A self-contained WebGL2 hero: a living oil-on-water field brightened into iridescent caustic filaments; move the cursor to bend the flow.",
"description_i18n": {
"zh-CN": "自包含 WebGL2 主视觉:活的油水场提亮为虹彩焦散丝线;移动光标扰动流场。",
"zh-TW": "自包含 WebGL2 主視覺:活的油水場提亮為虹彩焦散絲線;移動游標擾動流場。",
"en": "A self-contained WebGL2 hero: a living oil-on-water field brightened into iridescent caustic filaments; move the cursor to bend the flow."
},
"license": "MIT",
"author": {
"name": "Open Design",
"url": "https://github.com/nexu-io"
},
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/_official/examples/webgl-liquid-iridescence",
"tags": [
"example",
"first-party",
"hero",
"webgl",
"webgl2",
"shader",
"iridescent",
"liquid",
"generative",
"powered-preview",
"untitled"
],
"compat": {
"agentSkills": [
{
"path": "./SKILL.md"
}
]
},
"od": {
"kind": "scenario",
"taskKind": "new-generation",
"mode": "prototype",
"scenario": "design",
"surface": "web",
"preview": {
"type": "html",
"entry": "./example.html"
},
"useCase": {
"query": {
"en": "Build a self-contained WebGL2 hero of a living oil-on-water iridescent field with caustic filaments, cursor-reactive.",
"zh-CN": "构建自包含 WebGL2 主视觉:活的油水虹彩场加焦散丝线,光标可扰动。"
}
},
"context": {
"skills": [
{
"path": "./SKILL.md"
}
],
"designSystem": {
"primary": true
}
},
"pipeline": {
"stages": [
{
"id": "generate",
"atoms": [
"file-write",
"live-artifact"
]
}
]
},
"capabilities": [
"prompt:inject",
"fs:write"
]
}
}
@@ -0,0 +1,23 @@
MIT License
Effect © J0SUKE / Codrops. Ported to a single self-contained file and packaged for Open Design.
Original: https://github.com/J0SUKE/gsap-threejs-codrops (declared MIT).
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Sample imagery is original (AI-generated), not the author's Midjourney set.
@@ -0,0 +1,44 @@
---
name: webgl-pixel-reveal-gallery
description: |
A scroll-reactive masonry gallery (Three.js + GSAP) where each image develops from a pixel/grid dissolve on viewport entry, with a split-text heading and click-to-fullscreen Flip.
triggers:
- "pixel reveal gallery"
- "webgl gallery"
od:
mode: prototype
platform: web
scenario: design
preview:
type: html
entry: index.html
reload: debounce-100
design_system:
requires: false
craft:
requires: [animation-discipline]
---
# Pixel Reveal Gallery
Produce a single self-contained `index.html` — A scroll-reactive masonry gallery (Three.js + GSAP) where each image develops from a pixel/grid dissolve on viewport entry, with a split-text heading and click-to-fullscreen Flip.
## Why this is a powered artifact
Open Design detects `getContext('webgl2')` / heavy WebGL and renders this file in **powered preview** (a cross-origin-isolated iframe). The full GPU + scroll pipeline runs; no opaque-sandbox workarounds are needed.
## Resource map
```
webgl-pixel-reveal-gallery/
├── SKILL.md ← you're reading this
├── example.html ← the complete, working artifact (READ FIRST)
└── (assets, if any)
```
## Credits / attribution
- effect: J0SUKE / Codrops (MIT)
- imagery: Original (AI-generated)
Keep any bundled LICENSE and on-screen credit intact. Replace imagery only with license-clean assets (original / AI, Lummi.ai, Unsplash/Pexels — never scraped imagery).
@@ -0,0 +1,223 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pixel Reveal Gallery — Open Design</title>
<!--
Faithful single-file port of J0SUKE's "Pixel Image Effect" scroll-reveal
gallery (Codrops). Original: https://github.com/J0SUKE/gsap-threejs-codrops
The multi-page Astro/Barba build is condensed to one self-contained page:
masonry grid + WebGL pixel-grid reveal on scroll + split-text heading +
click-to-fullscreen (GSAP Flip). Three.js + GSAP via CDN, images original
(generated, not the author's Midjourney set). Runs in powered preview.
-->
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Manrope:wght@400;700;800&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{ box-sizing:border-box; }
:root{ --bg:#0d0d0f; --ink:#ececef; --mut:#8a8a92; --line:#242424; }
html,body{ margin:0; background:var(--bg); color:var(--ink);
font-family:'Manrope',system-ui,sans-serif; -webkit-font-smoothing:antialiased; }
#webgl{ position:fixed; inset:0; width:100%; height:100%; z-index:2; pointer-events:none; }
#app{ position:relative; z-index:1; }
.frame{ position:fixed; inset:0; z-index:5; pointer-events:none; padding:1.4rem 1.6rem;
display:flex; justify-content:space-between; align-items:flex-start;
font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.22em; text-transform:uppercase; color:var(--mut); }
.frame .dot{ width:7px;height:7px;border-radius:50%;background:#b8ff3c;box-shadow:0 0 12px #b8ff3c;display:inline-block;margin-right:8px;vertical-align:middle;}
.frame a{ color:var(--mut); pointer-events:auto; }
.wrap{ max-width:1180px; margin:0 auto; padding:0 1.6rem; }
header.intro{ padding:26vh 0 12vh; }
header.intro h1{ font-size:clamp(38px,7vw,104px); line-height:0.94; font-weight:800; letter-spacing:-0.03em; margin:0; }
header.intro h1 .l{ display:block; overflow:hidden; }
header.intro .meta{ margin-top:2rem; max-width:44ch; color:var(--mut); font-size:14px; line-height:1.5;
font-family:'IBM Plex Mono',monospace; letter-spacing:0.02em; }
.grid{ column-count:3; column-gap:2.2rem; padding-bottom:22vh; }
@media(max-width:900px){ .grid{ column-count:2; } }
@media(max-width:560px){ .grid{ column-count:1; } }
.item{ break-inside:avoid; margin:0 0 2.2rem; display:block; text-decoration:none; color:inherit; cursor:pointer; }
.item img{ width:100%; height:auto; display:block; opacity:0; } /* WebGL plane draws it */
.item .cap{ font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.06em; color:var(--mut);
display:flex; justify-content:space-between; margin-top:0.7rem; text-transform:uppercase; }
/* detail overlay */
#detail{ position:fixed; inset:0; z-index:8; display:none; align-items:center; justify-content:center;
background:rgba(8,8,10,0.0); backdrop-filter:blur(0px); }
#detail.on{ display:flex; }
#detail .stage{ position:relative; }
#detail img{ max-width:82vw; max-height:80vh; width:auto; height:auto; display:block; }
#detail .info{ position:absolute; left:0; bottom:-2.4rem; font-family:'IBM Plex Mono',monospace;
font-size:12px; letter-spacing:0.08em; color:var(--mut); text-transform:uppercase; display:flex; gap:1.6rem; }
#detail .close{ position:fixed; top:1.4rem; right:1.6rem; font-family:'IBM Plex Mono',monospace; font-size:11px;
letter-spacing:0.22em; text-transform:uppercase; color:var(--mut); cursor:pointer; }
</style>
</head>
<body>
<canvas id="webgl"></canvas>
<div class="frame">
<span><span class="dot"></span>Open Design · WebGL Preview</span>
<span>Effect: <a href="https://github.com/J0SUKE/gsap-threejs-codrops" target="_blank">J0SUKE / Codrops</a></span>
</div>
<div id="app"><div id="smooth-content">
<div class="wrap">
<header class="intro">
<h1 class="split"><span class="l">Northbound</span><span class="l">Field Records</span></h1>
<p class="meta">An invented archive of nine ascents, 1971–1979. Each frame resolves from a pixel grid as it enters the frame — scroll to develop, click to enlarge.</p>
</header>
<div class="grid" id="grid"></div>
</div>
</div></div>
<div id="detail"><div class="close">Close ✕</div><div class="stage"><img alt=""><div class="info"></div></div></div>
<script>
// ---- original gallery data (own captions; images generated, not the author's) ----
const DATA = [
{ url:'expedition/e1.jpg', name:'First Light Ascent', loc:'North Couloir', date:'April 14, 1971' },
{ url:'expedition/e2.jpg', name:'Camp Below the Col', loc:'Base II', date:'August 2, 1973' },
{ url:'expedition/e3.jpg', name:'Axe and Line', loc:'Equipment Cache', date:'June 9, 1974' },
{ url:'expedition/e4.jpg', name:'The Long Traverse', loc:'Glacier Basin', date:'July 27, 1975' },
{ url:'expedition/e5.jpg', name:'Overcast Plateau', loc:'High Tableland', date:'May 30, 1977' },
{ url:'expedition/e6.jpg', name:'Gloves and Chart', loc:'Wind Shelter', date:'September 6, 1979' },
];
const grid = document.getElementById('grid');
DATA.forEach((d,i)=>{
const a=document.createElement('a'); a.className='item'; a.href='#'; a.dataset.i=i;
a.innerHTML=`<img src="${d.url}" alt="${d.name}" crossorigin="anonymous">
<div class="cap"><span>${String(i+1).padStart(2,'0')} · ${d.name}</span><span>${d.date}</span></div>`;
grid.appendChild(a);
});
</script>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';
import gsap from 'https://cdn.jsdelivr.net/npm/gsap@3.13.0/index.js';
import { ScrollTrigger } from 'https://cdn.jsdelivr.net/npm/gsap@3.13.0/ScrollTrigger.js';
import { SplitText } from 'https://cdn.jsdelivr.net/npm/gsap@3.13.0/SplitText.js';
import { Flip } from 'https://cdn.jsdelivr.net/npm/gsap@3.13.0/Flip.js';
gsap.registerPlugin(ScrollTrigger, SplitText, Flip);
const VERT = `varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*viewMatrix*modelMatrix*vec4(position,1.0); }`;
const FRAG = `uniform sampler2D uTexture; varying vec2 vUv; uniform vec2 uResolution; uniform float uProgress;
uniform vec3 uColor; uniform vec2 uContainerRes;
float random(vec2 st){ return fract(sin(dot(st.xy,vec2(12.9898,78.233)))*43758.5453123); }
vec2 squaresGrid(vec2 vUv){ float cAx=uResolution.x/uResolution.y, cAy=uResolution.y/uResolution.x;
vec2 r=vec2(min(cAx,1.0),min(cAy,1.0));
return vec2(vUv.x*r.x+(1.0-r.x)*0.5, vUv.y*r.y+(1.0-r.y)*0.5); }
void main(){
float iAx=uResolution.x/uResolution.y, iAy=uResolution.y/uResolution.x;
float cAx=uContainerRes.x/uContainerRes.y, cAy=uContainerRes.y/uContainerRes.x;
vec2 ratio=vec2(min(cAx/iAx,1.0),min(cAy/iAy,1.0));
vec2 coverUvs=vec2(vUv.x*ratio.x+(1.0-ratio.x)*0.5, vUv.y*ratio.y+(1.0-ratio.y)*0.5);
vec2 squareUvs=squaresGrid(coverUvs);
float gridSize=floor(uContainerRes.x/20.);
vec2 grid=vec2(floor(squareUvs.x*gridSize)/gridSize, floor(squareUvs.y*gridSize)/gridSize);
vec4 gridTexture=vec4(uColor,0.);
vec4 texture=texture2D(uTexture,coverUvs);
float height=0.2;
float progress=(1.+height)-(uProgress*(1.+height+height));
float dist=1.-distance(grid.y,progress);
float clampedDist=smoothstep(height,0.,distance(grid.y,progress));
float randDist=step(1.-height*random(grid),dist);
dist=step(1.-height,dist);
float rand=random(grid);
float alpha=dist*(clampedDist+rand-0.5*(1.-randDist)); alpha=max(0.,alpha);
gridTexture.a=alpha;
texture.rgba*=step(progress,grid.y);
gl_FragColor=vec4(mix(texture,gridTexture,gridTexture.a));
}`;
// ---- Canvas (perspective cam mapping DOM px → world units) ----
const canvas=document.getElementById('webgl');
const scene=new THREE.Scene();
const camera=new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 100); camera.position.z=10;
const renderer=new THREE.WebGLRenderer({ canvas, alpha:true, antialias:true });
renderer.setPixelRatio(Math.min(2,devicePixelRatio)); renderer.setSize(innerWidth,innerHeight);
let sizes={};
function computeSizes(){ const fov=camera.fov*(Math.PI/180); const h=camera.position.z*Math.tan(fov/2)*2; sizes={width:h*camera.aspect, height:h}; }
computeSizes();
const loader=new THREE.TextureLoader();
// ---- Media (ported media.ts): a plane synced to an <img>'s DOM bounds ----
class Media {
constructor(el){ this.el=el; this.a=el.closest('a');
this.currentScroll=0; this.lastScroll=0;
this.geo=new THREE.PlaneGeometry(1,1);
this.mat=new THREE.ShaderMaterial({ vertexShader:VERT, fragmentShader:FRAG, transparent:true, uniforms:{
uTexture:{value:null}, uResolution:{value:new THREE.Vector2(1,1)}, uContainerRes:{value:new THREE.Vector2(1,1)},
uProgress:{value:0}, uColor:{value:new THREE.Color('#242424')} } });
this.mesh=new THREE.Mesh(this.geo,this.mat);
this.setBounds(); this.setDims(); this.setPos(); this.setTex();
scene.add(this.mesh);
}
setBounds(){ this.b=this.el.getBoundingClientRect(); this.node={w:this.b.width,h:this.b.height}; }
setDims(){ this.mesh.scale.x=this.node.w*sizes.width/innerWidth; this.mesh.scale.y=this.node.h*sizes.height/innerHeight; }
setPos(){ this.pos={ x:this.b.left*sizes.width/innerWidth, y:-this.b.top*sizes.height/innerHeight };
this.pos.x-=sizes.width/2; this.pos.x+=this.mesh.scale.x/2; this.pos.y-=this.mesh.scale.y/2; this.pos.y+=sizes.height/2;
this.mesh.position.x=this.pos.x; this.mesh.position.y=this.pos.y; }
setTex(){ this.mat.uniforms.uTexture.value=loader.load(this.el.src, ({image})=>{
this.mat.uniforms.uResolution.value.set(image.naturalWidth,image.naturalHeight);
this.mat.uniforms.uContainerRes.value.set(this.node.w,this.node.h);
this.mat.uniforms.uTexture.value.colorSpace=THREE.SRGBColorSpace; }); }
updateScroll(scrollY){ this.currentScroll=-scrollY*sizes.height/innerHeight;
const d=this.currentScroll-this.lastScroll; this.lastScroll=this.currentScroll;
this.pos.y-=d; this.mesh.position.y=this.pos.y; }
observe(){ this.st=gsap.to(this.mat.uniforms.uProgress, { value:1, duration:1.6, ease:'linear',
scrollTrigger:{ trigger:this.el, start:'top bottom', end:'bottom top', toggleActions:'play reset restart reset' } }); }
onResize(){ this.setBounds(); this.setDims(); this.setPos(); this.mat.uniforms.uContainerRes.value.set(this.node.w,this.node.h); }
}
// ---- boot after images load + fonts ----
let medias=[];
function boot(){
// Native scroll (robust across all browsers + the sandbox iframe). The fixed
// WebGL canvas locks its planes DIRECTLY to window.scrollY so each image stays
// pinned to its DOM cell — no ScrollSmoother normalizeScroll/transform hijack.
document.querySelectorAll('.item img').forEach(img=>medias.push(new Media(img)));
medias.forEach(m=>m.observe());
ScrollTrigger.refresh();
// heading split
const split=new SplitText('.split .l',{type:'lines',linesClass:'sl'});
gsap.from(split.lines,{ yPercent:120, duration:1.1, ease:'power3.out', stagger:0.12, delay:0.15 });
gsap.ticker.add(()=>{ const s=window.scrollY||document.documentElement.scrollTop||0;
medias.forEach(m=>m.updateScroll(s)); renderer.render(scene,camera); });
bindDetail();
}
function whenImagesReady(cb){
const imgs=[...document.querySelectorAll('.item img')]; let n=0; const total=imgs.length;
const done=()=>{ if(++n>=total) cb(); };
imgs.forEach(im=>{ if(im.complete&&im.naturalWidth) done(); else { im.addEventListener('load',done); im.addEventListener('error',done); } });
if(total===0) cb();
}
whenImagesReady(boot);
addEventListener('resize',()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight); computeSizes(); medias.forEach(m=>m.onResize()); ScrollTrigger.refresh(); });
// ---- click → fullscreen detail (in-page Flip, replaces Barba page nav) ----
function bindDetail(){
const detail=document.getElementById('detail'), dImg=detail.querySelector('img'), dInfo=detail.querySelector('.info');
document.querySelectorAll('.item').forEach(a=>{
a.addEventListener('click', e=>{
e.preventDefault(); const i=+a.dataset.i, d=DATA[i]; const srcImg=a.querySelector('img');
const r=srcImg.getBoundingClientRect();
dImg.src=d.url; dInfo.innerHTML=`<span>${d.name}</span><span>${d.loc}</span><span>${d.date}</span>`;
// 1) place the detail img exactly over the grid cell (start), 2) capture it,
// 3) release to its centered CSS layout (final), 4) Flip start→final.
gsap.set(dImg,{ position:'fixed', left:r.left, top:r.top, width:r.width, height:r.height, maxWidth:'none', maxHeight:'none', margin:0, zIndex:9 });
detail.classList.add('on');
const state=Flip.getState(dImg);
gsap.set(dImg,{ clearProps:'all' });
Flip.from(state,{ duration:0.8, ease:'power3.inOut', absolute:true });
gsap.fromTo(detail,{ backdropFilter:'blur(0px)', backgroundColor:'rgba(8,8,10,0)' },
{ backdropFilter:'blur(8px)', backgroundColor:'rgba(8,8,10,0.82)', duration:0.6 });
gsap.fromTo(dInfo,{ opacity:0, y:10 },{ opacity:1, y:0, duration:0.5, delay:0.35 });
});
});
const close=()=>{ gsap.to(detail,{ backdropFilter:'blur(0px)', backgroundColor:'rgba(8,8,10,0)', duration:0.45,
onComplete:()=>detail.classList.remove('on') }); gsap.to(dImg,{ opacity:0, duration:0.4, onComplete:()=>gsap.set(dImg,{opacity:1}) }); };
detail.querySelector('.close').addEventListener('click',close);
detail.addEventListener('click',e=>{ if(e.target===detail) close(); });
}
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

@@ -0,0 +1,104 @@
{
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
"specVersion": "1.0.0",
"name": "example-webgl-pixel-reveal-gallery",
"title": "Pixel Reveal Gallery",
"title_i18n": {
"zh-CN": "像素揭示画廊",
"zh-TW": "像素揭示畫廊",
"ja": "ピクセルリビール・ギャラリー",
"ko": "픽셀 리빌 갤러리",
"de": "Pixel-Reveal-Galerie",
"fr": "Galerie à révélation pixel",
"ru": "Пиксельная галерея",
"es": "Galería de revelado por píxeles",
"pt-BR": "Galeria de revelação em pixels",
"en": "Pixel Reveal Gallery"
},
"version": "0.1.0",
"description": "A scroll-reactive masonry gallery (Three.js + GSAP) where each image develops from a pixel/grid dissolve on viewport entry, with a split-text heading and click-to-fullscreen Flip.",
"description_i18n": {
"zh-CN": "滚动响应式瀑布流图廊(Three.js + GSAP):每图进入视口时从像素/网格溶解显影,配分行标题动画与点击 Flip 放大。",
"zh-TW": "滾動響應式瀑布流圖廊(Three.js + GSAP):每圖進入視口時從像素/網格溶解顯影,配分行標題與點擊 Flip 放大。",
"en": "A scroll-reactive masonry gallery (Three.js + GSAP) where each image develops from a pixel/grid dissolve on viewport entry, with a split-text heading and click-to-fullscreen Flip."
},
"license": "MIT",
"author": {
"name": "Open Design",
"url": "https://github.com/nexu-io"
},
"credits": [
{
"role": "effect",
"name": "J0SUKE / Codrops",
"license": "MIT"
},
{
"role": "imagery",
"name": "Original (AI-generated)"
}
],
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/_official/examples/webgl-pixel-reveal-gallery",
"tags": [
"example",
"first-party",
"gallery",
"webgl",
"three.js",
"gsap",
"scroll",
"reveal",
"masonry",
"powered-preview",
"untitled"
],
"compat": {
"agentSkills": [
{
"path": "./SKILL.md"
}
]
},
"od": {
"kind": "scenario",
"taskKind": "new-generation",
"mode": "prototype",
"scenario": "design",
"surface": "web",
"preview": {
"type": "html",
"entry": "./example.html"
},
"useCase": {
"query": {
"en": "Build a scroll-reactive masonry gallery where each image develops from a pixel/grid dissolve on entry, with a split-text heading and click-to-fullscreen Flip.",
"zh-CN": "构建滚动响应式瀑布流图廊,每图进入时从像素网格溶解显影,配分行标题与点击 Flip 放大。"
}
},
"context": {
"skills": [
{
"path": "./SKILL.md"
}
],
"designSystem": {
"primary": true
}
},
"pipeline": {
"stages": [
{
"id": "generate",
"atoms": [
"file-write",
"live-artifact"
]
}
]
},
"capabilities": [
"prompt:inject",
"fs:write"
]
}
}
@@ -0,0 +1,43 @@
---
name: webgl-voronoi-cells
description: |
A self-contained WebGL2 hero: a living Voronoi network with drifting feature points, palette-keyed cells and glowing boundaries; move the cursor to push the cells.
triggers:
- "voronoi cells"
- "webgl hero"
od:
mode: prototype
platform: web
scenario: design
preview:
type: html
entry: index.html
reload: debounce-100
design_system:
requires: false
craft:
requires: [animation-discipline]
---
# Voronoi Cells
Produce a single self-contained `index.html` — A self-contained WebGL2 hero: a living Voronoi network with drifting feature points, palette-keyed cells and glowing boundaries; move the cursor to push the cells.
## Why this is a powered artifact
Open Design detects `getContext('webgl2')` / heavy WebGL and renders this file in **powered preview** (a cross-origin-isolated iframe). The full GPU + scroll pipeline runs; no opaque-sandbox workarounds are needed.
## Resource map
```
webgl-voronoi-cells/
├── SKILL.md ← you're reading this
├── example.html ← the complete, working artifact (READ FIRST)
└── (assets, if any)
```
## Credits / attribution
- Original, self-contained (no third-party assets).
Keep any bundled LICENSE and on-screen credit intact. Replace imagery only with license-clean assets (original / AI, Lummi.ai, Unsplash/Pexels — never scraped imagery).
@@ -0,0 +1,95 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Voronoi Cells — Open Design</title>
<!--
Self-contained WebGL2 hero. An animated Voronoi network: jittered feature
points drift, cell interiors take a palette by cell id, and edges glow along
the F2-F1 boundary. The pointer pushes nearby cells. No meshes/textures/build.
-->
<style>
:root{ color-scheme:dark; }
html,body{ margin:0; height:100%; overflow:hidden; background:#060a09;
font-family:'Albert Sans',ui-sans-serif,system-ui,-apple-system,sans-serif; color:#eafff4; }
#gl{ position:fixed; inset:0; width:100%; height:100%; display:block; }
.overlay{ position:fixed; inset:0; z-index:2; pointer-events:none; display:flex; flex-direction:column; justify-content:space-between; padding:5vw; }
.top{ display:flex; justify-content:space-between; font-size:clamp(11px,1vw,14px); letter-spacing:0.32em; text-transform:uppercase; color:rgba(234,255,244,0.72); }
.top .dot{ width:8px; height:8px; border-radius:50%; background:#4dffa3; box-shadow:0 0 14px #4dffa3; display:inline-block; margin-right:10px; vertical-align:middle; }
h1{ margin:0; font-size:clamp(44px,11vw,168px); line-height:0.86; font-weight:800; letter-spacing:-0.04em; mix-blend-mode:overlay; }
h1 .thin{ font-weight:300; }
.bottom{ display:flex; justify-content:space-between; align-items:flex-end; gap:4vw; }
.sub{ font-size:clamp(14px,1.5vw,19px); max-width:42ch; color:rgba(234,255,244,0.82); line-height:1.45; margin:0; }
.badge{ font-size:11px; letter-spacing:0.28em; text-transform:uppercase; color:rgba(234,255,244,0.65); border:1px solid rgba(234,255,244,0.24); padding:9px 15px; border-radius:999px; backdrop-filter:blur(8px); white-space:nowrap; }
</style>
</head>
<body>
<canvas id="gl"></canvas>
<div class="overlay">
<div class="top"><span><span class="dot"></span>Open Design · WebGL Preview</span><span>Real-time · GPU</span></div>
<div class="bottom">
<div><h1>VORONOI<br><span class="thin">CELLS</span></h1>
<p class="sub">A living Voronoi network — drifting feature points, palette-keyed cells, glowing boundaries. Move the cursor to push the cells.</p></div>
<div class="badge">Move the cursor ↗</div>
</div>
</div>
<script>
(() => {
const canvas=document.getElementById('gl');
const gl=canvas.getContext('webgl2',{antialias:true});
if (!gl) { document.body.innerHTML='<p style="color:#fff;padding:2rem;font-family:sans-serif">WebGL2 not available.</p>'; return; }
const VERT=`#version 300 es
void main(){ vec2 p=vec2((gl_VertexID<<1)&2, gl_VertexID&2); gl_Position=vec4(p*2.0-1.0,0.0,1.0); }`;
const FRAG=`#version 300 es
precision highp float;
out vec4 o;
uniform vec2 u_res; uniform float u_time; uniform vec2 u_mouse;
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); }
vec3 pal(float t){ return 0.55+0.42*cos(6.28318*(vec3(0.6,0.55,0.75)*t+vec3(0.1,0.35,0.55))); }
void main(){
vec2 uv=(gl_FragCoord.xy-0.5*u_res)/u_res.y;
vec2 m=(u_mouse-0.5)*vec2(u_res.x/u_res.y,1.0);
float scale=4.5;
vec2 p=uv*scale;
// pointer push (smooth gaussian, no normalize singularity)
vec2 pull=(m*scale - p); float grab=exp(-dot(pull,pull)*0.35);
p += pull*grab*0.5;
vec2 g=floor(p), f=fract(p);
float f1=9.0, f2=9.0; vec2 id;
for(int y=-1;y<=1;y++) for(int x=-1;x<=1;x++){
vec2 lp=vec2(float(x),float(y));
vec2 pt=0.5+0.5*sin(u_time*0.6 + 6.2831*hash2(g+lp)); // drifting point
vec2 r=lp+pt-f; float d=dot(r,r);
if(d<f1){ f2=f1; f1=d; id=g+lp; } else if(d<f2){ f2=d; }
}
f1=sqrt(f1); f2=sqrt(f2);
float edge=smoothstep(0.0,0.06,f2-f1); // 0 on borders → glowing edges
float cellKey=fract(sin(dot(id,vec2(12.9,78.2)))*43758.5);
vec3 cell=pal(cellKey);
vec3 col=cell*(0.28+0.6*(1.0-f1)); // shade toward cell center
col=mix(vec3(0.85,1.0,0.92), col, edge); // bright cell walls
col+=vec3(0.3,1.0,0.7)*(1.0-edge)*0.5; // edge glow
col*=0.65+0.5*smoothstep(1.4,0.05,length(uv));
col=col/(col+0.75);
o=vec4(pow(max(col,0.0),vec3(0.85)),1.0);
}`;
function sh(t,s){const x=gl.createShader(t);gl.shaderSource(x,s);gl.compileShader(x);if(!gl.getShaderParameter(x,gl.COMPILE_STATUS))throw gl.getShaderInfoLog(x);return x;}
const pr=gl.createProgram();gl.attachShader(pr,sh(gl.VERTEX_SHADER,VERT));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,FRAG));gl.linkProgram(pr);
if(!gl.getProgramParameter(pr,gl.LINK_STATUS))throw gl.getProgramInfoLog(pr);
gl.useProgram(pr);
const uRes=gl.getUniformLocation(pr,'u_res'),uTime=gl.getUniformLocation(pr,'u_time'),uMouse=gl.getUniformLocation(pr,'u_mouse');
let mouse=[0.5,0.5],target=[0.5,0.5];
addEventListener('pointermove',e=>{target=[e.clientX/innerWidth,1.0-e.clientY/innerHeight];});
function resize(){const d=Math.min(devicePixelRatio||1,2);canvas.width=innerWidth*d|0;canvas.height=innerHeight*d|0;gl.viewport(0,0,canvas.width,canvas.height);}
addEventListener('resize',resize);resize();
const t0=performance.now();
function frame(now){ mouse[0]+=(target[0]-mouse[0])*0.06; mouse[1]+=(target[1]-mouse[1])*0.06;
gl.uniform2f(uRes,canvas.width,canvas.height); gl.uniform1f(uTime,(now-t0)/1000); gl.uniform2f(uMouse,mouse[0],mouse[1]);
gl.drawArrays(gl.TRIANGLES,0,3); requestAnimationFrame(frame); }
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
@@ -0,0 +1,93 @@
{
"$schema": "https://open-design.ai/schemas/plugin.v1.json",
"specVersion": "1.0.0",
"name": "example-webgl-voronoi-cells",
"title": "Voronoi Cells",
"title_i18n": {
"zh-CN": "Voronoi 细胞",
"zh-TW": "Voronoi 細胞",
"ja": "ボロノイ・セル",
"ko": "보로노이 셀",
"de": "Voronoi-Zellen",
"fr": "Cellules de Voronoï",
"ru": "Ячейки Вороного",
"es": "Celdas de Voronoi",
"pt-BR": "Células de Voronoi",
"en": "Voronoi Cells"
},
"version": "0.1.0",
"description": "A self-contained WebGL2 hero: a living Voronoi network with drifting feature points, palette-keyed cells and glowing boundaries; move the cursor to push the cells.",
"description_i18n": {
"zh-CN": "自包含 WebGL2 主视觉:活的 Voronoi 网络,漂移特征点、按调色板着色的细胞与发光边界;移动光标推挤细胞。",
"zh-TW": "自包含 WebGL2 主視覺:活的 Voronoi 網絡,漂移特徵點、按調色板著色的細胞與發光邊界;移動游標推擠細胞。",
"en": "A self-contained WebGL2 hero: a living Voronoi network with drifting feature points, palette-keyed cells and glowing boundaries; move the cursor to push the cells."
},
"license": "MIT",
"author": {
"name": "Open Design",
"url": "https://github.com/nexu-io"
},
"homepage": "https://github.com/nexu-io/open-design/tree/main/plugins/_official/examples/webgl-voronoi-cells",
"tags": [
"example",
"first-party",
"hero",
"webgl",
"webgl2",
"shader",
"voronoi",
"cells",
"generative",
"powered-preview",
"untitled"
],
"compat": {
"agentSkills": [
{
"path": "./SKILL.md"
}
]
},
"od": {
"kind": "scenario",
"taskKind": "new-generation",
"mode": "prototype",
"scenario": "design",
"surface": "web",
"preview": {
"type": "html",
"entry": "./example.html"
},
"useCase": {
"query": {
"en": "Build a self-contained WebGL2 hero of a living Voronoi network with drifting points, palette-keyed cells and glowing edges, cursor-reactive.",
"zh-CN": "构建自包含 WebGL2 主视觉:活的 Voronoi 网络,漂移点、调色细胞与发光边界,光标可推挤。"
}
},
"context": {
"skills": [
{
"path": "./SKILL.md"
}
],
"designSystem": {
"primary": true
}
},
"pipeline": {
"stages": [
{
"id": "generate",
"atoms": [
"file-write",
"live-artifact"
]
}
]
},
"capabilities": [
"prompt:inject",
"fs:write"
]
}
}