feat:新增文件内容
This commit is contained in:
448
test.html
Normal file
448
test.html
Normal file
@@ -0,0 +1,448 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Shader Preview Builder</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #0c1018;
|
||||
--card: #141b27;
|
||||
--line: #253147;
|
||||
--text: #e8edf7;
|
||||
--muted: #96a6c4;
|
||||
--accent: #4fa3ff;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font: 14px/1.4 "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
color: var(--text);
|
||||
background: radial-gradient(circle at 20% 10%, #1a2740, var(--bg) 55%);
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 1240px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 4px 0 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: color-mix(in oklab, var(--card) 88%, black 12%);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#glCanvas {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
display: block;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.preview-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.preview-meta {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.row > label {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
button {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #0f1624;
|
||||
color: var(--text);
|
||||
padding: 8px;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 200px;
|
||||
resize: vertical;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
.two {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
background: linear-gradient(90deg, #2d69c7, #2ea4db);
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: #1d2739;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="app">
|
||||
<header>
|
||||
<h1 class="title">Shader 预览与交互结构</h1>
|
||||
<p class="desc">仅保留预览和交互,方便快速构建。</p>
|
||||
</header>
|
||||
|
||||
<section class="layout">
|
||||
<article class="card preview-wrap">
|
||||
<canvas id="glCanvas" width="1280" height="720"></canvas>
|
||||
<div class="preview-bar">
|
||||
<span class="preview-meta" id="fpsText">FPS: --</span>
|
||||
<div class="two" style="width: 220px">
|
||||
<button class="secondary" id="toggleBtn" type="button">暂停</button>
|
||||
<button id="fullscreenBtn" type="button">全屏</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="card panel">
|
||||
<div class="row">
|
||||
<label for="template">模板</label>
|
||||
<select id="template">
|
||||
<option value="fire">Fire</option>
|
||||
<option value="ripple">Ripple</option>
|
||||
<option value="distort">Distort</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="speed">速度 <span id="speedVal">1.00</span></label>
|
||||
<input id="speed" type="range" min="0" max="3" step="0.01" value="1" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="scale">缩放 <span id="scaleVal">4.00</span></label>
|
||||
<input id="scale" type="range" min="0.5" max="12" step="0.1" value="4" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="intensity">强度 <span id="intensityVal">1.00</span></label>
|
||||
<input id="intensity" type="range" min="0" max="2" step="0.01" value="1" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="fragCode">Fragment Shader(可编辑)</label>
|
||||
<textarea id="fragCode"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="two">
|
||||
<button type="button" id="applyBtn">应用 Shader</button>
|
||||
<button type="button" class="secondary" id="resetBtn">重置模板</button>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById("glCanvas");
|
||||
const gl = canvas.getContext("webgl");
|
||||
|
||||
const speed = document.getElementById("speed");
|
||||
const scale = document.getElementById("scale");
|
||||
const intensity = document.getElementById("intensity");
|
||||
const speedVal = document.getElementById("speedVal");
|
||||
const scaleVal = document.getElementById("scaleVal");
|
||||
const intensityVal = document.getElementById("intensityVal");
|
||||
const fragCode = document.getElementById("fragCode");
|
||||
const template = document.getElementById("template");
|
||||
const fpsText = document.getElementById("fpsText");
|
||||
const toggleBtn = document.getElementById("toggleBtn");
|
||||
|
||||
if (!gl) {
|
||||
alert("当前浏览器不支持 WebGL");
|
||||
}
|
||||
|
||||
const vertexSource = `
|
||||
attribute vec2 a_pos;
|
||||
varying vec2 v_uv;
|
||||
void main() {
|
||||
v_uv = a_pos * 0.5 + 0.5;
|
||||
gl_Position = vec4(a_pos, 0.0, 1.0);
|
||||
}`;
|
||||
|
||||
const templates = {
|
||||
fire: `
|
||||
precision mediump float;
|
||||
varying vec2 v_uv;
|
||||
uniform float u_time;
|
||||
uniform float u_speed;
|
||||
uniform float u_scale;
|
||||
uniform float u_intensity;
|
||||
|
||||
float hash21(vec2 p){ p = fract(p*vec2(123.34,456.21)); p += dot(p,p+78.23); return fract(p.x*p.y); }
|
||||
float noise2d(vec2 p){
|
||||
vec2 i=floor(p), f=fract(p);
|
||||
float a=hash21(i), b=hash21(i+vec2(1.,0.)), c=hash21(i+vec2(0.,1.)), d=hash21(i+vec2(1.,1.));
|
||||
vec2 u=f*f*(3.-2.*f);
|
||||
return mix(a,b,u.x)+(c-a)*u.y*(1.-u.x)+(d-b)*u.x*u.y;
|
||||
}
|
||||
|
||||
void main(){
|
||||
vec2 uv=v_uv;
|
||||
uv.y += u_time*u_speed*0.12;
|
||||
float n=noise2d(uv*u_scale+vec2(0.,u_time*0.6));
|
||||
float f=smoothstep(0.2,1.0,n+(1.0-uv.y)*0.8);
|
||||
vec3 col=mix(vec3(0.85,0.1,0.02),vec3(1.0,0.75,0.08),f);
|
||||
gl_FragColor=vec4(col, clamp(f*u_intensity,0.0,1.0));
|
||||
}`,
|
||||
ripple: `
|
||||
precision mediump float;
|
||||
varying vec2 v_uv;
|
||||
uniform float u_time;
|
||||
uniform float u_speed;
|
||||
uniform float u_scale;
|
||||
uniform float u_intensity;
|
||||
|
||||
void main(){
|
||||
vec2 uv=v_uv-0.5;
|
||||
float d=length(uv);
|
||||
float w=sin((d*u_scale-u_time*u_speed*3.0)*6.2831)*0.5+0.5;
|
||||
vec3 col=mix(vec3(0.05,0.2,0.5),vec3(0.4,0.85,1.0),w);
|
||||
float a=clamp((1.0-d*1.6)*u_intensity,0.0,1.0);
|
||||
gl_FragColor=vec4(col,a);
|
||||
}`,
|
||||
distort: `
|
||||
precision mediump float;
|
||||
varying vec2 v_uv;
|
||||
uniform float u_time;
|
||||
uniform float u_speed;
|
||||
uniform float u_scale;
|
||||
uniform float u_intensity;
|
||||
|
||||
float hash21(vec2 p){ p = fract(p*vec2(123.34,456.21)); p += dot(p,p+78.23); return fract(p.x*p.y); }
|
||||
float noise2d(vec2 p){
|
||||
vec2 i=floor(p), f=fract(p);
|
||||
float a=hash21(i), b=hash21(i+vec2(1.,0.)), c=hash21(i+vec2(0.,1.)), d=hash21(i+vec2(1.,1.));
|
||||
vec2 u=f*f*(3.-2.*f);
|
||||
return mix(a,b,u.x)+(c-a)*u.y*(1.-u.x)+(d-b)*u.x*u.y;
|
||||
}
|
||||
|
||||
void main(){
|
||||
vec2 uv=v_uv;
|
||||
float off=noise2d(uv*u_scale+u_time*u_speed)-0.5;
|
||||
uv += vec2(off*0.18,off*0.1);
|
||||
float shade=noise2d(uv*(u_scale*0.7)+11.3);
|
||||
vec3 col=mix(vec3(0.05,0.05,0.08),vec3(0.8,0.95,1.0),shade);
|
||||
gl_FragColor=vec4(col, clamp((0.65+shade*0.35)*u_intensity,0.0,1.0));
|
||||
}`
|
||||
};
|
||||
|
||||
function createShader(type, source) {
|
||||
const shader = gl.createShader(type);
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
const info = gl.getShaderInfoLog(shader);
|
||||
gl.deleteShader(shader);
|
||||
throw new Error(info);
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
function createProgram(vsSource, fsSource) {
|
||||
const vs = createShader(gl.VERTEX_SHADER, vsSource);
|
||||
const fs = createShader(gl.FRAGMENT_SHADER, fsSource);
|
||||
const program = gl.createProgram();
|
||||
gl.attachShader(program, vs);
|
||||
gl.attachShader(program, fs);
|
||||
gl.linkProgram(program);
|
||||
gl.deleteShader(vs);
|
||||
gl.deleteShader(fs);
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
const info = gl.getProgramInfoLog(program);
|
||||
gl.deleteProgram(program);
|
||||
throw new Error(info);
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
let program;
|
||||
let loc = {};
|
||||
|
||||
function buildProgram(fragmentSource) {
|
||||
const next = createProgram(vertexSource, fragmentSource);
|
||||
if (program) gl.deleteProgram(program);
|
||||
program = next;
|
||||
loc = {
|
||||
a_pos: gl.getAttribLocation(program, "a_pos"),
|
||||
u_time: gl.getUniformLocation(program, "u_time"),
|
||||
u_speed: gl.getUniformLocation(program, "u_speed"),
|
||||
u_scale: gl.getUniformLocation(program, "u_scale"),
|
||||
u_intensity: gl.getUniformLocation(program, "u_intensity")
|
||||
};
|
||||
}
|
||||
|
||||
const quad = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
|
||||
|
||||
function loadTemplate(name) {
|
||||
fragCode.value = templates[name];
|
||||
}
|
||||
|
||||
function updateLabels() {
|
||||
speedVal.textContent = Number(speed.value).toFixed(2);
|
||||
scaleVal.textContent = Number(scale.value).toFixed(2);
|
||||
intensityVal.textContent = Number(intensity.value).toFixed(2);
|
||||
}
|
||||
|
||||
template.addEventListener("change", () => {
|
||||
loadTemplate(template.value);
|
||||
applyShader();
|
||||
});
|
||||
|
||||
[speed, scale, intensity].forEach((el) => el.addEventListener("input", updateLabels));
|
||||
|
||||
function applyShader() {
|
||||
try {
|
||||
buildProgram(fragCode.value);
|
||||
} catch (err) {
|
||||
alert("Shader 编译失败:\n" + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("applyBtn").addEventListener("click", applyShader);
|
||||
document.getElementById("resetBtn").addEventListener("click", () => {
|
||||
speed.value = "1";
|
||||
scale.value = "4";
|
||||
intensity.value = "1";
|
||||
template.value = "fire";
|
||||
loadTemplate("fire");
|
||||
updateLabels();
|
||||
applyShader();
|
||||
});
|
||||
|
||||
document.getElementById("fullscreenBtn").addEventListener("click", () => {
|
||||
if (canvas.requestFullscreen) canvas.requestFullscreen();
|
||||
});
|
||||
|
||||
let playing = true;
|
||||
toggleBtn.addEventListener("click", () => {
|
||||
playing = !playing;
|
||||
toggleBtn.textContent = playing ? "暂停" : "继续";
|
||||
});
|
||||
|
||||
function resize() {
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const w = Math.max(1, Math.floor(rect.width * dpr));
|
||||
const h = Math.max(1, Math.floor(rect.height * dpr));
|
||||
if (canvas.width !== w || canvas.height !== h) {
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
gl.viewport(0, 0, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
let start = performance.now();
|
||||
let prev = start;
|
||||
let frame = 0;
|
||||
let acc = 0;
|
||||
|
||||
function draw(now) {
|
||||
resize();
|
||||
const dt = now - prev;
|
||||
prev = now;
|
||||
|
||||
frame += 1;
|
||||
acc += dt;
|
||||
if (acc >= 500) {
|
||||
fpsText.textContent = `FPS: ${(1000 * frame / acc).toFixed(1)}`;
|
||||
frame = 0;
|
||||
acc = 0;
|
||||
}
|
||||
|
||||
if (playing) {
|
||||
gl.clearColor(0, 0, 0, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
gl.useProgram(program);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
|
||||
gl.enableVertexAttribArray(loc.a_pos);
|
||||
gl.vertexAttribPointer(loc.a_pos, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.uniform1f(loc.u_time, (now - start) / 1000);
|
||||
gl.uniform1f(loc.u_speed, Number(speed.value));
|
||||
gl.uniform1f(loc.u_scale, Number(scale.value));
|
||||
gl.uniform1f(loc.u_intensity, Number(intensity.value));
|
||||
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
loadTemplate("fire");
|
||||
updateLabels();
|
||||
applyShader();
|
||||
requestAnimationFrame(draw);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user