summaryrefslogtreecommitdiff
path: root/tools/shader_editor/index.html
diff options
context:
space:
mode:
Diffstat (limited to 'tools/shader_editor/index.html')
-rw-r--r--tools/shader_editor/index.html767
1 files changed, 767 insertions, 0 deletions
diff --git a/tools/shader_editor/index.html b/tools/shader_editor/index.html
new file mode 100644
index 0000000..d74b9ff
--- /dev/null
+++ b/tools/shader_editor/index.html
@@ -0,0 +1,767 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>WGSL Shader Editor</title>
+ <style>
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", monospace;
+ background: #1e1e1e;
+ color: #d4d4d4;
+ overflow: hidden;
+ height: 100vh;
+}
+
+.container {
+ display: flex;
+ height: 100vh;
+}
+
+.preview-pane {
+ flex: 0 0 70%;
+ background: #252526;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+}
+
+#preview-canvas {
+ flex: 1;
+ width: 100%;
+ height: auto;
+ background: #000;
+}
+
+.fps-counter {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ background: rgba(0, 0, 0, 0.7);
+ padding: 5px 10px;
+ border-radius: 3px;
+ font-size: 12px;
+ color: #0f0;
+ font-family: monospace;
+}
+
+.error-overlay {
+ position: absolute;
+ top: 50px;
+ left: 10px;
+ right: 10px;
+ background: rgba(200, 0, 0, 0.9);
+ color: #fff;
+ padding: 15px;
+ border-radius: 5px;
+ font-family: monospace;
+ font-size: 12px;
+ white-space: pre-wrap;
+ max-height: 50%;
+ overflow-y: auto;
+}
+
+.error-overlay.hidden {
+ display: none;
+}
+
+.controls {
+ background: #2d2d30;
+ padding: 15px;
+ border-top: 1px solid #3e3e42;
+}
+
+.control-group {
+ margin-bottom: 10px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.control-group label {
+ font-size: 13px;
+}
+
+.control-group button {
+ background: #0e639c;
+ color: #fff;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 13px;
+}
+
+.control-group button:hover {
+ background: #1177bb;
+}
+
+.control-group input[type="number"],
+.control-group select {
+ background: #3c3c3c;
+ color: #d4d4d4;
+ border: 1px solid #3e3e42;
+ padding: 4px 8px;
+ border-radius: 3px;
+ font-size: 13px;
+}
+
+.control-group input[type="range"] {
+ flex: 1;
+ max-width: 200px;
+}
+
+.control-group input[type="checkbox"] {
+ margin: 0 5px;
+}
+
+.progress-bar {
+ flex: 1;
+ height: 8px;
+ background: #3c3c3c;
+ border-radius: 4px;
+ overflow: hidden;
+ max-width: 300px;
+}
+
+.progress-fill {
+ height: 100%;
+ background: #0e639c;
+ transition: width 0.1s linear;
+}
+
+.editor-pane {
+ flex: 0 0 30%;
+ background: #1e1e1e;
+ display: flex;
+ flex-direction: column;
+ border-left: 1px solid #3e3e42;
+}
+
+.editor-header {
+ background: #2d2d30;
+ padding: 10px;
+ border-bottom: 1px solid #3e3e42;
+ display: flex;
+ gap: 10px;
+}
+
+.editor-header button {
+ background: #0e639c;
+ color: #fff;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 13px;
+}
+
+.editor-header button:hover {
+ background: #1177bb;
+}
+
+#code-editor {
+ flex: 1;
+ width: 100%;
+ background: #1e1e1e;
+ color: #d4d4d4;
+ border: none;
+ padding: 15px;
+ font-family: 'Courier New', monospace;
+ font-size: 13px;
+ line-height: 1.5;
+ resize: none;
+ outline: none;
+}
+
+.snippets-panel {
+ background: #252526;
+ border-top: 1px solid #3e3e42;
+ padding: 15px;
+ max-height: 40%;
+ overflow-y: auto;
+}
+
+.snippets-panel.hidden {
+ display: none;
+}
+
+.snippets-panel h3 {
+ font-size: 14px;
+ margin-bottom: 10px;
+ color: #4ec9b0;
+}
+
+.snippet-item {
+ padding: 5px;
+ cursor: pointer;
+ border-radius: 3px;
+ font-family: monospace;
+ font-size: 12px;
+}
+
+.snippet-item:hover {
+ background: #2a2d2e;
+}
+ </style>
+</head>
+<body>
+ <div class="container">
+ <div class="preview-pane">
+ <canvas id="preview-canvas"></canvas>
+ <div id="error-overlay" class="error-overlay hidden"></div>
+ <div class="fps-counter" id="fps-counter">FPS: 0</div>
+
+ <div class="controls">
+ <div class="control-group">
+ <button id="play-pause-btn">▶ Play</button>
+ <button id="reset-time-btn">Reset Time</button>
+ </div>
+
+ <div class="control-group">
+ <label>Loop Time: <span id="loop-time-value">0.00</span></label>
+ <div class="progress-bar">
+ <div id="loop-time-bar" class="progress-fill"></div>
+ </div>
+ </div>
+
+ <div class="control-group">
+ <label>Loop Period: <input type="number" id="loop-period" min="0.1" max="10" step="0.1" value="2.0">s</label>
+ </div>
+
+ <div class="control-group">
+ <label>Time: <span id="time-value">0.00</span>s</label>
+ </div>
+
+ <div class="control-group">
+ <label>Audio Peak: <input type="range" id="audio-peak" min="0" max="1" step="0.01" value="0.5"></label>
+ <span id="audio-peak-value">0.50</span>
+ <label><input type="checkbox" id="audio-pulse"> Auto Pulse</label>
+ </div>
+
+ <div class="control-group">
+ <label>Resolution:
+ <select id="resolution">
+ <option value="512x512">512×512</option>
+ <option value="1024x1024" selected>1024×1024</option>
+ <option value="1920x1080">1920×1080</option>
+ <option value="1280x720">1280×720</option>
+ </select>
+ </label>
+ </div>
+ </div>
+ </div>
+
+ <div class="editor-pane">
+ <div class="editor-header">
+ <input type="file" id="file-input" accept=".wgsl" style="display: none;">
+ <button id="load-btn">Load .wgsl</button>
+ <button id="save-btn">Save .wgsl</button>
+ <button id="snippets-btn">📚 Snippets</button>
+ </div>
+
+ <textarea id="code-editor" spellcheck="false"></textarea>
+
+ <div id="snippets-panel" class="snippets-panel hidden">
+ <h3>Available Snippets</h3>
+ <div id="snippets-list"></div>
+ </div>
+ </div>
+ </div>
+
+ <script>
+// Shader Composer
+class ShaderComposer {
+ constructor() {
+ this.snippets = new Map();
+ }
+
+ registerSnippet(name, code) {
+ this.snippets.set(name, code);
+ }
+
+ resolveRecursive(source, included, substitutions) {
+ const lines = source.split('\n');
+ const result = [];
+
+ for (const line of lines) {
+ if (line.trim().startsWith('#include ')) {
+ const match = line.match(/#include\s+"([^"]+)"/);
+ if (match) {
+ let name = match[1];
+
+ if (substitutions && substitutions.has(name)) {
+ name = substitutions.get(name);
+ }
+
+ if (!included.has(name)) {
+ included.add(name);
+ const snippet = this.snippets.get(name);
+ if (snippet) {
+ result.push(`// --- Included: ${name} ---`);
+ const resolved = this.resolveRecursive(snippet, included, substitutions);
+ result.push(resolved);
+ result.push(`// --- End Include: ${name} ---`);
+ } else {
+ result.push(`// ERROR: Snippet not found: ${name}`);
+ }
+ }
+ }
+ } else {
+ result.push(line);
+ }
+ }
+
+ return result.join('\n');
+ }
+
+ compose(mainCode, dependencies = [], substitutions = null) {
+ const result = ['// Generated by ShaderComposer', ''];
+ const included = new Set();
+
+ for (let dep of dependencies) {
+ let name = dep;
+ if (substitutions && substitutions.has(name)) {
+ name = substitutions.get(name);
+ }
+
+ if (!included.has(name)) {
+ included.add(name);
+ const snippet = this.snippets.get(name);
+ if (snippet) {
+ result.push(`// --- Dependency: ${name} ---`);
+ result.push(this.resolveRecursive(snippet, included, substitutions));
+ result.push('');
+ }
+ }
+ }
+
+ result.push('// --- Main Code ---');
+ result.push(this.resolveRecursive(mainCode, included, substitutions));
+
+ return result.join('\n');
+ }
+}
+
+// WebGPU Preview
+class WebGPUPreview {
+ constructor(canvas) {
+ this.canvas = canvas;
+ this.device = null;
+ this.context = null;
+ this.pipeline = null;
+ this.uniformBuffer = null;
+ this.bindGroup = null;
+ this.vertexBuffer = null;
+ this.sampler = null;
+ this.texture = null;
+ this.textureView = null;
+ this.isPlaying = false;
+ this.startTime = performance.now();
+ this.pauseTime = 0;
+ this.currentTime = 0;
+ this.loopPeriod = 2.0;
+ this.audioPeak = 0.5;
+ this.autoPulse = false;
+ this.lastFrameTime = 0;
+ this.frameCount = 0;
+ this.fps = 0;
+ this.errorCallback = null;
+ }
+
+ async init() {
+ if (!navigator.gpu) {
+ throw new Error('WebGPU not supported');
+ }
+
+ const adapter = await navigator.gpu.requestAdapter();
+ if (!adapter) {
+ throw new Error('No GPU adapter found');
+ }
+
+ this.device = await adapter.requestDevice();
+ this.context = this.canvas.getContext('webgpu');
+
+ const format = navigator.gpu.getPreferredCanvasFormat();
+ this.context.configure({
+ device: this.device,
+ format: format,
+ alphaMode: 'opaque',
+ });
+
+ const vertices = new Float32Array([-1, -1, 3, -1, -1, 3]);
+ this.vertexBuffer = this.device.createBuffer({
+ size: vertices.byteLength,
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
+ mappedAtCreation: true,
+ });
+ new Float32Array(this.vertexBuffer.getMappedRange()).set(vertices);
+ this.vertexBuffer.unmap();
+
+ this.uniformBuffer = this.device.createBuffer({
+ size: 32,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+ });
+
+ this.sampler = this.device.createSampler({
+ magFilter: 'linear',
+ minFilter: 'linear',
+ });
+
+ this.texture = this.device.createTexture({
+ size: [1, 1],
+ format: 'rgba8unorm',
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
+ });
+ this.textureView = this.texture.createView();
+
+ const blackPixel = new Uint8Array([0, 0, 0, 255]);
+ this.device.queue.writeTexture(
+ { texture: this.texture },
+ blackPixel,
+ { bytesPerRow: 4 },
+ [1, 1]
+ );
+
+ await this.loadShader(this.getDefaultShader());
+ }
+
+ getDefaultShader() {
+ return `@group(0) @binding(0) var smplr: sampler;
+@group(0) @binding(1) var txt: texture_2d<f32>;
+
+struct CommonUniforms {
+ resolution: vec2<f32>,
+ _pad0: f32,
+ _pad1: f32,
+ aspect_ratio: f32,
+ time: f32,
+ beat: f32,
+ audio_intensity: f32,
+};
+@group(0) @binding(2) var<uniform> uniforms: CommonUniforms;
+
+@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
+ var pos = array<vec2<f32>, 3>(
+ vec2<f32>(-1, -1),
+ vec2<f32>(3, -1),
+ vec2<f32>(-1, 3)
+ );
+ return vec4<f32>(pos[i], 0.0, 1.0);
+}
+
+@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> {
+ let uv = p.xy / uniforms.resolution;
+ let c = vec3<f32>(
+ 0.5 + 0.5 * sin(uniforms.time + uv.x * 3.0),
+ 0.5 + 0.5 * cos(uniforms.time + uv.y * 3.0),
+ 0.5 + 0.5 * sin(uniforms.beat * 6.28)
+ );
+ return vec4<f32>(c * uniforms.audio_intensity, 1.0);
+}`;
+ }
+
+ async loadShader(shaderCode) {
+ try {
+ const shaderModule = this.device.createShaderModule({ code: shaderCode });
+
+ const info = await shaderModule.getCompilationInfo();
+ if (info.messages.length > 0) {
+ const errors = info.messages.filter(m => m.type === 'error');
+ if (errors.length > 0) {
+ const errorText = errors.map(e => `Line ${e.lineNum}: ${e.message}`).join('\n');
+ throw new Error(errorText);
+ }
+ }
+
+ this.pipeline = this.device.createRenderPipeline({
+ layout: 'auto',
+ vertex: {
+ module: shaderModule,
+ entryPoint: 'vs_main',
+ buffers: [{
+ arrayStride: 8,
+ attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }],
+ }],
+ },
+ fragment: {
+ module: shaderModule,
+ entryPoint: 'fs_main',
+ targets: [{ format: navigator.gpu.getPreferredCanvasFormat() }],
+ },
+ primitive: { topology: 'triangle-list' },
+ });
+
+ this.bindGroup = this.device.createBindGroup({
+ layout: this.pipeline.getBindGroupLayout(0),
+ entries: [
+ { binding: 0, resource: this.sampler },
+ { binding: 1, resource: this.textureView },
+ { binding: 2, resource: { buffer: this.uniformBuffer } },
+ ],
+ });
+
+ if (this.errorCallback) this.errorCallback(null);
+ } catch (error) {
+ if (this.errorCallback) this.errorCallback(error.message);
+ throw error;
+ }
+ }
+
+ updateUniforms() {
+ const now = performance.now();
+ if (!this.isPlaying) {
+ this.currentTime = this.pauseTime;
+ } else {
+ this.currentTime = (now - this.startTime) / 1000;
+ }
+
+ const loopTime = (this.currentTime % this.loopPeriod) / this.loopPeriod;
+ const audioPeak = this.autoPulse ? 0.5 + 0.5 * Math.sin(this.currentTime * Math.PI) : this.audioPeak;
+
+ const uniformData = new Float32Array([
+ this.canvas.width, this.canvas.height, 0, 0,
+ this.canvas.width / this.canvas.height,
+ this.currentTime, loopTime, audioPeak,
+ ]);
+
+ this.device.queue.writeBuffer(this.uniformBuffer, 0, uniformData);
+
+ this.frameCount++;
+ if (now - this.lastFrameTime >= 1000) {
+ this.fps = this.frameCount;
+ this.frameCount = 0;
+ this.lastFrameTime = now;
+ }
+
+ return { time: this.currentTime, loopTime, audioPeak };
+ }
+
+ render() {
+ if (!this.pipeline) return;
+
+ const stats = this.updateUniforms();
+
+ const commandEncoder = this.device.createCommandEncoder();
+ const textureView = this.context.getCurrentTexture().createView();
+
+ const renderPass = commandEncoder.beginRenderPass({
+ colorAttachments: [{
+ view: textureView,
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
+ loadOp: 'clear',
+ storeOp: 'store',
+ }],
+ });
+
+ renderPass.setPipeline(this.pipeline);
+ renderPass.setBindGroup(0, this.bindGroup);
+ renderPass.setVertexBuffer(0, this.vertexBuffer);
+ renderPass.draw(3);
+ renderPass.end();
+
+ this.device.queue.submit([commandEncoder.finish()]);
+
+ return stats;
+ }
+
+ play() {
+ this.isPlaying = true;
+ this.startTime = this.pauseTime > 0 ? performance.now() - this.pauseTime * 1000 : performance.now();
+ }
+
+ pause() {
+ this.isPlaying = false;
+ this.pauseTime = this.currentTime;
+ }
+
+ resetTime() {
+ this.startTime = performance.now();
+ this.pauseTime = 0;
+ this.currentTime = 0;
+ }
+
+ setResolution(width, height) {
+ this.canvas.width = width;
+ this.canvas.height = height;
+ }
+
+ setLoopPeriod(period) { this.loopPeriod = period; }
+ setAudioPeak(peak) { this.audioPeak = peak; }
+ setAutoPulse(enabled) { this.autoPulse = enabled; }
+ setErrorCallback(callback) { this.errorCallback = callback; }
+ getFPS() { return this.fps; }
+}
+
+// Main App
+const composer = new ShaderComposer();
+composer.registerSnippet('common_uniforms', `struct CommonUniforms {
+ resolution: vec2<f32>,
+ _pad0: f32,
+ _pad1: f32,
+ aspect_ratio: f32,
+ time: f32,
+ beat: f32,
+ audio_intensity: f32,
+};`);
+
+const canvas = document.getElementById('preview-canvas');
+const preview = new WebGPUPreview(canvas);
+const editor = document.getElementById('code-editor');
+const playPauseBtn = document.getElementById('play-pause-btn');
+const resetTimeBtn = document.getElementById('reset-time-btn');
+const loopPeriodInput = document.getElementById('loop-period');
+const audioPeakInput = document.getElementById('audio-peak');
+const audioPeakValue = document.getElementById('audio-peak-value');
+const autoPulseCheckbox = document.getElementById('audio-pulse');
+const resolutionSelect = document.getElementById('resolution');
+const loadBtn = document.getElementById('load-btn');
+const saveBtn = document.getElementById('save-btn');
+const snippetsBtn = document.getElementById('snippets-btn');
+const fileInput = document.getElementById('file-input');
+const errorOverlay = document.getElementById('error-overlay');
+const fpsCounter = document.getElementById('fps-counter');
+const timeValue = document.getElementById('time-value');
+const loopTimeValue = document.getElementById('loop-time-value');
+const loopTimeBar = document.getElementById('loop-time-bar');
+const snippetsPanel = document.getElementById('snippets-panel');
+const snippetsList = document.getElementById('snippets-list');
+
+let composeTimeout = null;
+
+async function init() {
+ try {
+ await preview.init();
+
+ preview.setErrorCallback((error) => {
+ if (error) {
+ errorOverlay.textContent = error;
+ errorOverlay.classList.remove('hidden');
+ } else {
+ errorOverlay.classList.add('hidden');
+ }
+ });
+
+ updateResolution();
+ editor.value = preview.getDefaultShader();
+ renderLoop();
+ } catch (error) {
+ errorOverlay.textContent = `Init failed: ${error.message}`;
+ errorOverlay.classList.remove('hidden');
+ }
+}
+
+function renderLoop() {
+ const stats = preview.render();
+ if (stats) {
+ fpsCounter.textContent = `FPS: ${preview.getFPS()}`;
+ timeValue.textContent = stats.time.toFixed(2);
+ loopTimeValue.textContent = stats.loopTime.toFixed(2);
+ loopTimeBar.style.width = `${stats.loopTime * 100}%`;
+ audioPeakValue.textContent = stats.audioPeak.toFixed(2);
+ }
+ requestAnimationFrame(renderLoop);
+}
+
+playPauseBtn.addEventListener('click', () => {
+ if (preview.isPlaying) {
+ preview.pause();
+ playPauseBtn.textContent = '▶ Play';
+ } else {
+ preview.play();
+ playPauseBtn.textContent = '⏸ Pause';
+ }
+});
+
+resetTimeBtn.addEventListener('click', () => preview.resetTime());
+loopPeriodInput.addEventListener('input', (e) => preview.setLoopPeriod(parseFloat(e.target.value)));
+audioPeakInput.addEventListener('input', (e) => preview.setAudioPeak(parseFloat(e.target.value)));
+autoPulseCheckbox.addEventListener('change', (e) => {
+ preview.setAutoPulse(e.target.checked);
+ audioPeakInput.disabled = e.target.checked;
+});
+resolutionSelect.addEventListener('change', updateResolution);
+
+function updateResolution() {
+ const [width, height] = resolutionSelect.value.split('x').map(Number);
+ preview.setResolution(width, height);
+}
+
+editor.addEventListener('input', () => {
+ clearTimeout(composeTimeout);
+ composeTimeout = setTimeout(composeAndLoad, 300);
+});
+
+editor.addEventListener('keydown', (e) => {
+ if (e.key === 'Tab') {
+ e.preventDefault();
+ const start = editor.selectionStart;
+ editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(editor.selectionEnd);
+ editor.selectionStart = editor.selectionEnd = start + 4;
+ }
+});
+
+async function composeAndLoad() {
+ try {
+ const composed = composer.compose(editor.value);
+ await preview.loadShader(composed);
+ } catch (error) {
+ console.error('Compose failed:', error);
+ }
+}
+
+loadBtn.addEventListener('click', () => {
+ fileInput.value = '';
+ fileInput.click();
+});
+
+fileInput.addEventListener('change', (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ editor.value = event.target.result;
+ composeAndLoad();
+ };
+ reader.readAsText(file);
+ }
+});
+
+saveBtn.addEventListener('click', () => {
+ const blob = new Blob([editor.value], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
+ a.download = `shader_${timestamp}.wgsl`;
+ a.click();
+ URL.revokeObjectURL(url);
+});
+
+snippetsBtn.addEventListener('click', () => snippetsPanel.classList.toggle('hidden'));
+
+document.addEventListener('keydown', (e) => {
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
+ e.preventDefault();
+ saveBtn.click();
+ }
+ if ((e.ctrlKey || e.metaKey) && e.key === 'o') {
+ e.preventDefault();
+ loadBtn.click();
+ }
+ if (e.key === ' ' && !editor.matches(':focus')) {
+ e.preventDefault();
+ playPauseBtn.click();
+ }
+});
+
+init();
+ </script>
+</body>
+</html>