Skip to content

Instantly share code, notes, and snippets.

@yankooliveira
Last active May 11, 2025 10:56
Show Gist options
  • Select an option

  • Save yankooliveira/d12a6e1a20930b485b687d8847f380d1 to your computer and use it in GitHub Desktop.

Select an option

Save yankooliveira/d12a6e1a20930b485b687d8847f380d1 to your computer and use it in GitHub Desktop.
Shadertoy Channel Uploader bookmarklet
// Shadertoy is awesome but it's hard to set your own textures. This adds a small bar to the top that lets you
// upload images to each channel.
//
// Installation: create a new bookmark and copy the following code into the `url` field:
javascript:(function(){const s=[{filter:'mipmap',wrap:'clamp',vflip:'true',srgb:'true',internal:'byte'},{filter:'linear',wrap:'clamp',vflip:'true',srgb:'false',internal:'byte'},{filter:'nearest',wrap:'clamp',vflip:'true',srgb:'false',internal:'byte'},{filter:'mipmap',wrap:'clamp',vflip:'true',srgb:'true',internal:'byte'}];const N=4;if(typeof gShaderToy==='undefined'){console.error("ShaderToy environment (gShaderToy) not found.");alert("ShaderToy environment (gShaderToy) not found. Ensure you are on a ShaderToy page.");return;}let ec=document.getElementById('customUploadControls');if(ec){ec.remove();}let ef=document.getElementById('customFileInput');if(ef){ef.remove();}const u=document.createElement('div');u.style.cssText='padding:10px;background-color:#2a2a2a;border-bottom:1px solid #444;display:flex;gap:10px;align-items:center;';u.id='customUploadControls';const h=document.querySelector('#header');if(h){h.parentNode.insertBefore(u,h.nextSibling);}else{document.body.insertBefore(u,document.body.firstChild);}const f=document.createElement('input');f.type='file';f.accept='image/png, image/jpeg, image/jpg';f.style.display='none';f.id='customFileInput';let t=0;f.onchange=(e)=>{if(e.target.files&&e.target.files[0]){const l=e.target.files[0];const r=new FileReader();const c=t;r.onload=(v)=>{const d=v.target.result;console.log(`Read file for iChannel${c}, size: ${d.length} bytes`);try{let i=c+1;if(gShaderToy.mEffect&&gShaderToy.mEffect.mPasses&&gShaderToy.mEffect.mPasses[0]&&gShaderToy.mEffect.mPasses[0].mInputs[c]){i=gShaderToy.mEffect.mPasses[0].mInputs[c].mInfo.mID;console.log(`Using existing texture ID ${i} for iChannel${c}`);}else{console.warn(`Could not get existing texture ID for iChannel${c}, using fallback ID ${i}.`);}const o={mSrc:d,mType:'texture',mID:i,mSampler:s[c]||s[0]};console.log(`Calling gShaderToy.SetTexture(${c}, ...)`,o);gShaderToy.SetTexture(c,o);console.log(`Texture set for iChannel${c}.`);}catch(x){console.error(`Error setting texture for iChannel${c}:`,x);alert(`Error setting texture for iChannel${c}: ${x.message}`);}};r.onerror=(x)=>{console.error("FileReader error:",x);alert("Error reading file.");};r.readAsDataURL(l);}e.target.value=null;};document.body.appendChild(f);for(let i=0;i<N;i++){const b=document.createElement('button');b.textContent=`Upload iChannel${i}`;b.style.cssText='padding:5px 10px;cursor:pointer;background-color:#444;color:#eee;border:1px solid #666;border-radius:3px;';b.onclick=()=>{t=i;f.click();};u.appendChild(b);}console.log(`Custom upload buttons added for iChannel0-${N-1}.`);})();
// Here's the full, un-minified version:
(function() {
// --- Configuration ---
const samplerSettings = [
// Channel 0 (Color) - Assume sRGB, use mipmaps, clamp, vflip
{ filter: 'mipmap', wrap: 'clamp', vflip: 'true', srgb: 'true', internal: 'byte' },
// Channel 1 (Normal) - Not sRGB, linear filtering, clamp, vflip
{ filter: 'linear', wrap: 'clamp', vflip: 'true', srgb: 'false', internal: 'byte' },
// Channel 2 (Depth) - Not sRGB, nearest filtering often best, clamp, vflip
{ filter: 'nearest', wrap: 'clamp', vflip: 'true', srgb: 'false', internal: 'byte' },
// Channel 3 (e.g., Aux/Data/Another Color) - Assume sRGB, mipmaps, clamp, vflip (like Channel 0)
{ filter: 'mipmap', wrap: 'clamp', vflip: 'true', srgb: 'true', internal: 'byte' }
];
const NUM_CHANNELS = 4; // Explicitly 4 channels
// --- End Configuration ---
if (typeof gShaderToy === 'undefined') {
console.error("ShaderToy environment (gShaderToy) not found. Are you on a ShaderToy page?");
alert("ShaderToy environment (gShaderToy) not found. Ensure you are on a ShaderToy page.");
return;
}
// Remove existing controls if they exist (for re-running the bookmarklet)
const existingControls = document.getElementById('customUploadControls');
if (existingControls) {
existingControls.remove();
}
const existingFileInput = document.getElementById('customFileInput');
if (existingFileInput) {
existingFileInput.remove();
}
// Create a container for the buttons
const uploadContainer = document.createElement('div');
uploadContainer.style.padding = '10px';
uploadContainer.style.backgroundColor = '#2a2a2a';
uploadContainer.style.borderBottom = '1px solid #444';
uploadContainer.style.display = 'flex';
uploadContainer.style.gap = '10px';
uploadContainer.style.alignItems = 'center';
uploadContainer.id = 'customUploadControls';
// Inject the container near the top
const header = document.querySelector('#header');
if (header) {
header.parentNode.insertBefore(uploadContainer, header.nextSibling);
} else {
document.body.insertBefore(uploadContainer, document.body.firstChild); // Fallback
}
// Create a hidden file input element
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/png, image/jpeg, image/jpg';
fileInput.style.display = 'none';
fileInput.id = 'customFileInput'; // Add an ID for potential removal
// Store the target channel index when a button is clicked
let targetChannel = 0;
// Function to handle file selection
fileInput.onchange = (event) => {
if (event.target.files && event.target.files[0]) {
const file = event.target.files[0];
const reader = new FileReader();
const currentChannel = targetChannel; // Capture channel index for the closure
reader.onload = (e) => {
const base64DataUrl = e.target.result;
console.log(`Read file for iChannel${currentChannel}, size: ${base64DataUrl.length} bytes`);
try {
let textureId = currentChannel + 1; // Fallback ID
if (gShaderToy.mEffect && gShaderToy.mEffect.mPasses && gShaderToy.mEffect.mPasses[0] && gShaderToy.mEffect.mPasses[0].mInputs[currentChannel]) {
textureId = gShaderToy.mEffect.mPasses[0].mInputs[currentChannel].mInfo.mID;
console.log(`Using existing texture ID ${textureId} for iChannel${currentChannel}`);
} else {
console.warn(`Could not get existing texture ID for iChannel${currentChannel}, using fallback ID ${textureId}. This might cause issues.`);
}
const textureOptions = {
mSrc: base64DataUrl,
mType: 'texture',
mID: textureId,
mSampler: samplerSettings[currentChannel] || samplerSettings[0] // Use specific or default
};
console.log(`Calling gShaderToy.SetTexture(${currentChannel}, ...)`, textureOptions);
gShaderToy.SetTexture(currentChannel, textureOptions);
console.log(`Texture hopefully set for iChannel${currentChannel}.`);
// gShaderToy.Compile(); // Uncomment if needed, but often SetTexture is enough
} catch (error) {
console.error(`Error setting texture for iChannel${currentChannel}:`, error);
alert(`Error setting texture for iChannel${currentChannel}: ${error.message}`);
}
};
reader.onerror = (e) => {
console.error("FileReader error:", e);
alert("Error reading file.");
};
reader.readAsDataURL(file);
}
event.target.value = null; // Reset file input
};
document.body.appendChild(fileInput);
// Create upload buttons for channels 0, 1, 2, 3
for (let i = 0; i < NUM_CHANNELS; i++) {
const button = document.createElement('button');
button.textContent = `Upload iChannel${i}`;
button.style.padding = '5px 10px';
button.style.cursor = 'pointer';
button.style.backgroundColor = '#444';
button.style.color = '#eee';
button.style.border = '1px solid #666';
button.style.borderRadius = '3px';
button.onclick = () => {
targetChannel = i;
fileInput.click();
};
uploadContainer.appendChild(button);
}
console.log(`Custom upload buttons added for iChannel0-${NUM_CHANNELS-1}.`);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment