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>
@@ -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>
|
||||
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
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).
|
||||
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
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 & 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>
|
||||
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
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"
|
||||
]
|
||||
}
|
||||
}
|
||||