UNPKG

shadertoy2webgl

Version:

Convert ShaderToy shaders to WebGL2 - CLI and library

122 lines (100 loc) 3.93 kB
import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const TEMPLATES_DIR = path.join(__dirname, '..', 'templates'); const BASE = 'https://www.shadertoy.com'; const readTemplate = name => fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf8'); const extractTitle = html => { const titleMatch = html.match(/<div[^>]*id="editorHeaderText"[^>]*>[\s\S]*?<span[^>]*>([^<]+)<\/span>/); return titleMatch ? titleMatch[1].trim() : ''; }; const createInitialJson = (shaderId, title) => ({ id: shaderId, title, uniforms: { iResolution: "vec3", iTime: "float", iFrame: "float", iMouse: "vec4" } }); const createFormData = shaderId => { const formData = new FormData(); formData.append('s', JSON.stringify({ shaders: [shaderId] })); formData.append('nt', '1'); formData.append('nl', '1'); formData.append('np', '1'); return formData; }; const validateShaderData = (shaderData, shaderId) => { const [{ renderpass: [{ code }] }] = shaderData; if (!code) { throw new Error(`Invalid shader data received for '${shaderId}'`); } const shaderCode = code.trim(); if (!shaderCode) { throw new Error(`Shader '${shaderId}' has no code`); } return shaderCode; }; export const fetchShader = async (shaderId, options = { force: false }) => { if (fs.existsSync(shaderId)) { if (!options.force) { throw new Error(`Directory '${shaderId}' already exists. Use --force to overwrite.`); } fs.rmSync(shaderId, { recursive: true, force: true }); } fs.mkdirSync(shaderId, { recursive: true }); const shaderFile = path.join(shaderId, 'shader.json'); const response = await fetch(`${BASE}/view/${shaderId}`, { headers: { 'User-Agent': 'Mozilla/5.0' } }); if (!response.ok) { if (response.status === 404) { throw new Error(`Shader '${shaderId}' does not exist on ShaderToy`); } throw new Error(`Failed to fetch shader: ${response.statusText}`); } const html = await response.text(); const title = extractTitle(html); const initialJson = createInitialJson(shaderId, title); const shaderResponse = await fetch(`${BASE}/shadertoy`, { method: 'POST', headers: { 'origin': BASE, 'referer': `${BASE}/view/${shaderId}`, 'user-agent': 'Mozilla/5.0' }, body: createFormData(shaderId) }); if (!shaderResponse.ok) { throw new Error(`Failed to fetch shader code: ${shaderResponse.statusText}`); } const shaderData = await shaderResponse.json(); const shaderCode = validateShaderData(shaderData, shaderId); const finalJson = { ...initialJson, code: shaderCode }; fs.writeFileSync(shaderFile, JSON.stringify(finalJson, null, 2)); return finalJson; }; export const convertShader = async shader => { const shaderDir = shader.id; const htmlFile = path.join(shaderDir, 'index.html'); const jsFile = path.join(shaderDir, 'shader.js'); const webglCode = readTemplate('webgl2.js') .replace('${shaderCode}', shader.code); const htmlTemplate = readTemplate('webgl2.html') .replace('${shaderTitle}', shader.title || 'Untitled Shader') .replace('${webglCode}', webglCode); fs.writeFileSync(htmlFile, htmlTemplate); fs.writeFileSync(jsFile, webglCode); return { html: htmlFile, js: jsFile }; }; export const shadertoy2webgl = async (shaderId, options = { force: false }) => { try { const shader = await fetchShader(shaderId, options); return await convertShader(shader); } catch (error) { throw new Error(`Failed to process shader: ${error.message}`); } };