bluedither 1.0.24 → 1.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -107,6 +107,10 @@ export default async function install(args) {
107
107
  if (existsSync(middlewarePath)) {
108
108
  filesToCopy.push(['fine-tuner/dev-middleware.js', 'dev-middleware.js']);
109
109
  }
110
+ const sidecarPath = resolve(BLUEDITHER_ROOT, 'fine-tuner', 'sidecar.js');
111
+ if (existsSync(sidecarPath)) {
112
+ filesToCopy.push(['fine-tuner/sidecar.js', 'sidecar.js']);
113
+ }
110
114
 
111
115
  let copied = 0;
112
116
  for (const [src, dest] of filesToCopy) {
@@ -167,10 +171,14 @@ $ARGUMENTS
167
171
 
168
172
  To apply the theme, use Claude Code:
169
173
  /apply-theme [description of your site]
170
-
174
+ ${framework === 'vanilla' ? `
175
+ For vanilla projects, start the dev server:
176
+ node bluedither/sidecar.js [port]
177
+ This serves your files AND handles "Commit Changes" saves.
178
+ ` : `
171
179
  The tuner panel loads automatically in dev mode.
172
180
  Click "Commit Changes" to save tweaks to tokens.json.
173
- `);
181
+ `} `);
174
182
  }
175
183
 
176
184
  function autoWireVitePlugin(targetDir) {
@@ -280,6 +288,7 @@ async function installFromRegistry(slug, targetDir) {
280
288
  ['fine-tuner/tuner.css', 'bluedither-tuner.css'],
281
289
  ['fine-tuner/inject.js', 'bluedither-tuner-inject.js'],
282
290
  ['fine-tuner/dev-middleware.js', 'dev-middleware.js'],
291
+ ['fine-tuner/sidecar.js', 'sidecar.js'],
283
292
  ];
284
293
  for (const [src, dest] of tunerFiles) {
285
294
  const srcPath = resolve(BLUEDITHER_ROOT, src);
@@ -338,8 +347,12 @@ $ARGUMENTS
338
347
 
339
348
  To apply the theme, use Claude Code:
340
349
  /apply-theme [description of your site]
341
-
350
+ ${framework === 'vanilla' ? `
351
+ For vanilla projects, start the dev server:
352
+ node bluedither/sidecar.js [port]
353
+ This serves your files AND handles "Commit Changes" saves.
354
+ ` : `
342
355
  The tuner panel loads automatically in dev mode.
343
356
  Click "Commit Changes" to save tweaks to tokens.json.
344
- `);
357
+ `} `);
345
358
  }
@@ -148,7 +148,7 @@ void main() {
148
148
 
149
149
  v_imageUV += .5;
150
150
  v_imageUV.y = 1. - v_imageUV.y;
151
- }`,z=8294400,X=class{constructor(e,o,t,i,n=0,s=0,r=2,l=z,m=[]){a(this,"parentElement");a(this,"canvasElement");a(this,"gl");a(this,"program",null);a(this,"uniformLocations",{});a(this,"fragmentShader");a(this,"rafId",null);a(this,"lastRenderTime",0);a(this,"currentFrame",0);a(this,"speed",0);a(this,"currentSpeed",0);a(this,"providedUniforms");a(this,"mipmaps",[]);a(this,"hasBeenDisposed",!1);a(this,"resolutionChanged",!0);a(this,"textures",new Map);a(this,"minPixelRatio");a(this,"maxPixelCount");a(this,"isSafari",oe());a(this,"uniformCache",{});a(this,"textureUnitMap",new Map);a(this,"initProgram",()=>{let e=$(this.gl,Z,this.fragmentShader);e&&(this.program=e)});a(this,"setupPositionAttribute",()=>{let e=this.gl.getAttribLocation(this.program,"a_position"),o=this.gl.createBuffer();this.gl.bindBuffer(this.gl.ARRAY_BUFFER,o);let t=[-1,-1,1,-1,-1,1,-1,1,1,-1,1,1];this.gl.bufferData(this.gl.ARRAY_BUFFER,new Float32Array(t),this.gl.STATIC_DRAW),this.gl.enableVertexAttribArray(e),this.gl.vertexAttribPointer(e,2,this.gl.FLOAT,!1,0,0)});a(this,"setupUniforms",()=>{let e={u_time:this.gl.getUniformLocation(this.program,"u_time"),u_pixelRatio:this.gl.getUniformLocation(this.program,"u_pixelRatio"),u_resolution:this.gl.getUniformLocation(this.program,"u_resolution")};Object.entries(this.providedUniforms).forEach(([o,t])=>{if(e[o]=this.gl.getUniformLocation(this.program,o),t instanceof HTMLImageElement){let i=`${o}AspectRatio`;e[i]=this.gl.getUniformLocation(this.program,i)}}),this.uniformLocations=e});a(this,"renderScale",1);a(this,"parentWidth",0);a(this,"parentHeight",0);a(this,"parentDevicePixelWidth",0);a(this,"parentDevicePixelHeight",0);a(this,"devicePixelsSupported",!1);a(this,"resizeObserver",null);a(this,"setupResizeObserver",()=>{this.resizeObserver=new ResizeObserver(([e])=>{if(e?.borderBoxSize[0]){let o=e.devicePixelContentBoxSize?.[0];o!==void 0&&(this.devicePixelsSupported=!0,this.parentDevicePixelWidth=o.inlineSize,this.parentDevicePixelHeight=o.blockSize),this.parentWidth=e.borderBoxSize[0].inlineSize,this.parentHeight=e.borderBoxSize[0].blockSize}this.handleResize()}),this.resizeObserver.observe(this.parentElement)});a(this,"handleVisualViewportChange",()=>{this.resizeObserver?.disconnect(),this.setupResizeObserver()});a(this,"handleResize",()=>{let e=0,o=0,t=Math.max(1,window.devicePixelRatio),i=visualViewport?.scale??1;if(this.devicePixelsSupported){let u=Math.max(1,this.minPixelRatio/t);e=this.parentDevicePixelWidth*u*i,o=this.parentDevicePixelHeight*u*i}else{let u=Math.max(t,this.minPixelRatio)*i;if(this.isSafari){let d=te();u*=Math.max(1,d)}e=Math.round(this.parentWidth)*u,o=Math.round(this.parentHeight)*u}let n=Math.sqrt(this.maxPixelCount)/Math.sqrt(e*o),s=Math.min(1,n),r=Math.round(e*s),l=Math.round(o*s),m=r/Math.round(this.parentWidth);(this.canvasElement.width!==r||this.canvasElement.height!==l||this.renderScale!==m)&&(this.renderScale=m,this.canvasElement.width=r,this.canvasElement.height=l,this.resolutionChanged=!0,this.gl.viewport(0,0,this.gl.canvas.width,this.gl.canvas.height),this.render(performance.now()))});a(this,"render",e=>{if(this.hasBeenDisposed)return;if(this.program===null){console.warn("Tried to render before program or gl was initialized");return}let o=e-this.lastRenderTime;this.lastRenderTime=e,this.currentSpeed!==0&&(this.currentFrame+=o*this.currentSpeed),this.gl.clear(this.gl.COLOR_BUFFER_BIT),this.gl.useProgram(this.program),this.gl.uniform1f(this.uniformLocations.u_time,this.currentFrame*.001),this.resolutionChanged&&(this.gl.uniform2f(this.uniformLocations.u_resolution,this.gl.canvas.width,this.gl.canvas.height),this.gl.uniform1f(this.uniformLocations.u_pixelRatio,this.renderScale),this.resolutionChanged=!1),this.gl.drawArrays(this.gl.TRIANGLES,0,6),this.currentSpeed!==0?this.requestRender():this.rafId=null});a(this,"requestRender",()=>{this.rafId!==null&&cancelAnimationFrame(this.rafId),this.rafId=requestAnimationFrame(this.render)});a(this,"setTextureUniform",(e,o)=>{if(!o.complete||o.naturalWidth===0)throw new Error(`Paper Shaders: image for uniform ${e} must be fully loaded`);let t=this.textures.get(e);t&&this.gl.deleteTexture(t),this.textureUnitMap.has(e)||this.textureUnitMap.set(e,this.textureUnitMap.size);let i=this.textureUnitMap.get(e);this.gl.activeTexture(this.gl.TEXTURE0+i);let n=this.gl.createTexture();this.gl.bindTexture(this.gl.TEXTURE_2D,n),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_S,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_T,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MIN_FILTER,this.gl.LINEAR),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MAG_FILTER,this.gl.LINEAR),this.gl.texImage2D(this.gl.TEXTURE_2D,0,this.gl.RGBA,this.gl.RGBA,this.gl.UNSIGNED_BYTE,o),this.mipmaps.includes(e)&&(this.gl.generateMipmap(this.gl.TEXTURE_2D),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MIN_FILTER,this.gl.LINEAR_MIPMAP_LINEAR));let s=this.gl.getError();if(s!==this.gl.NO_ERROR||n===null){console.error("Paper Shaders: WebGL error when uploading texture:",s);return}this.textures.set(e,n);let r=this.uniformLocations[e];if(r){this.gl.uniform1i(r,i);let l=`${e}AspectRatio`,m=this.uniformLocations[l];if(m){let u=o.naturalWidth/o.naturalHeight;this.gl.uniform1f(m,u)}}});a(this,"areUniformValuesEqual",(e,o)=>e===o?!0:Array.isArray(e)&&Array.isArray(o)&&e.length===o.length?e.every((t,i)=>this.areUniformValuesEqual(t,o[i])):!1);a(this,"setUniformValues",e=>{this.gl.useProgram(this.program),Object.entries(e).forEach(([o,t])=>{let i=t;if(t instanceof HTMLImageElement&&(i=`${t.src.slice(0,200)}|${t.naturalWidth}x${t.naturalHeight}`),this.areUniformValuesEqual(this.uniformCache[o],i))return;this.uniformCache[o]=i;let n=this.uniformLocations[o];if(!n){console.warn(`Uniform location for ${o} not found`);return}if(t instanceof HTMLImageElement)this.setTextureUniform(o,t);else if(Array.isArray(t)){let s=null,r=null;if(t[0]!==void 0&&Array.isArray(t[0])){let l=t[0].length;if(t.every(m=>m.length===l))s=t.flat(),r=l;else{console.warn(`All child arrays must be the same length for ${o}`);return}}else s=t,r=s.length;switch(r){case 2:this.gl.uniform2fv(n,s);break;case 3:this.gl.uniform3fv(n,s);break;case 4:this.gl.uniform4fv(n,s);break;case 9:this.gl.uniformMatrix3fv(n,!1,s);break;case 16:this.gl.uniformMatrix4fv(n,!1,s);break;default:console.warn(`Unsupported uniform array length: ${r}`)}}else typeof t=="number"?this.gl.uniform1f(n,t):typeof t=="boolean"?this.gl.uniform1i(n,t?1:0):console.warn(`Unsupported uniform type for ${o}: ${typeof t}`)})});a(this,"getCurrentFrame",()=>this.currentFrame);a(this,"setFrame",e=>{this.currentFrame=e,this.lastRenderTime=performance.now(),this.render(performance.now())});a(this,"setSpeed",(e=1)=>{this.speed=e,this.setCurrentSpeed(document.hidden?0:e)});a(this,"setCurrentSpeed",e=>{this.currentSpeed=e,this.rafId===null&&e!==0&&(this.lastRenderTime=performance.now(),this.rafId=requestAnimationFrame(this.render)),this.rafId!==null&&e===0&&(cancelAnimationFrame(this.rafId),this.rafId=null)});a(this,"setMaxPixelCount",(e=z)=>{this.maxPixelCount=e,this.handleResize()});a(this,"setMinPixelRatio",(e=2)=>{this.minPixelRatio=e,this.handleResize()});a(this,"setUniforms",e=>{this.setUniformValues(e),this.providedUniforms={...this.providedUniforms,...e},this.render(performance.now())});a(this,"handleDocumentVisibilityChange",()=>{this.setCurrentSpeed(document.hidden?0:this.speed)});a(this,"dispose",()=>{this.hasBeenDisposed=!0,this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.gl&&this.program&&(this.textures.forEach(e=>{this.gl.deleteTexture(e)}),this.textures.clear(),this.gl.deleteProgram(this.program),this.program=null,this.gl.bindBuffer(this.gl.ARRAY_BUFFER,null),this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER,null),this.gl.bindRenderbuffer(this.gl.RENDERBUFFER,null),this.gl.bindFramebuffer(this.gl.FRAMEBUFFER,null),this.gl.getError()),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),visualViewport?.removeEventListener("resize",this.handleVisualViewportChange),document.removeEventListener("visibilitychange",this.handleDocumentVisibilityChange),this.uniformLocations={},this.canvasElement.remove(),delete this.parentElement.paperShaderMount});if(e instanceof HTMLElement)this.parentElement=e;else throw new Error("Paper Shaders: parent element must be an HTMLElement");if(!document.querySelector("style[data-paper-shader]")){let x=document.createElement("style");x.innerHTML=ee,x.setAttribute("data-paper-shader",""),document.head.prepend(x)}let u=document.createElement("canvas");this.canvasElement=u,this.parentElement.prepend(u),this.fragmentShader=o,this.providedUniforms=t,this.mipmaps=m,this.currentFrame=s,this.minPixelRatio=r,this.maxPixelCount=l;let d=u.getContext("webgl2",i);if(!d)throw new Error("Paper Shaders: WebGL is not supported in this browser");this.gl=d,this.initProgram(),this.setupPositionAttribute(),this.setupUniforms(),this.setUniformValues(this.providedUniforms),this.setupResizeObserver(),visualViewport?.addEventListener("resize",this.handleVisualViewportChange),this.setSpeed(n),this.parentElement.setAttribute("data-paper-shader",""),this.parentElement.paperShaderMount=this,document.addEventListener("visibilitychange",this.handleDocumentVisibilityChange)}};function O(e,o,t){let i=e.createShader(o);return i?(e.shaderSource(i,t),e.compileShader(i),e.getShaderParameter(i,e.COMPILE_STATUS)?i:(console.error("An error occurred compiling the shaders: "+e.getShaderInfoLog(i)),e.deleteShader(i),null)):null}function $(e,o,t){let i=e.getShaderPrecisionFormat(e.FRAGMENT_SHADER,e.MEDIUM_FLOAT),n=i?i.precision:null;n&&n<23&&(o=o.replace(/precision\s+(lowp|mediump)\s+float;/g,"precision highp float;"),t=t.replace(/precision\s+(lowp|mediump)\s+float/g,"precision highp float").replace(/\b(uniform|varying|attribute)\s+(lowp|mediump)\s+(\w+)/g,"$1 highp $3"));let s=O(e,e.VERTEX_SHADER,o),r=O(e,e.FRAGMENT_SHADER,t);if(!s||!r)return null;let l=e.createProgram();return l?(e.attachShader(l,s),e.attachShader(l,r),e.linkProgram(l),e.getProgramParameter(l,e.LINK_STATUS)?(e.detachShader(l,s),e.detachShader(l,r),e.deleteShader(s),e.deleteShader(r),l):(console.error("Unable to initialize the shader program: "+e.getProgramInfoLog(l)),e.deleteProgram(l),e.deleteShader(s),e.deleteShader(r),null)):null}var ee=`@layer paper-shaders {
151
+ }`,z=8294400,X=class{constructor(e,o,t,r,l=0,s=0,n=2,i=z,u=[]){a(this,"parentElement");a(this,"canvasElement");a(this,"gl");a(this,"program",null);a(this,"uniformLocations",{});a(this,"fragmentShader");a(this,"rafId",null);a(this,"lastRenderTime",0);a(this,"currentFrame",0);a(this,"speed",0);a(this,"currentSpeed",0);a(this,"providedUniforms");a(this,"mipmaps",[]);a(this,"hasBeenDisposed",!1);a(this,"resolutionChanged",!0);a(this,"textures",new Map);a(this,"minPixelRatio");a(this,"maxPixelCount");a(this,"isSafari",oe());a(this,"uniformCache",{});a(this,"textureUnitMap",new Map);a(this,"initProgram",()=>{let e=$(this.gl,Z,this.fragmentShader);e&&(this.program=e)});a(this,"setupPositionAttribute",()=>{let e=this.gl.getAttribLocation(this.program,"a_position"),o=this.gl.createBuffer();this.gl.bindBuffer(this.gl.ARRAY_BUFFER,o);let t=[-1,-1,1,-1,-1,1,-1,1,1,-1,1,1];this.gl.bufferData(this.gl.ARRAY_BUFFER,new Float32Array(t),this.gl.STATIC_DRAW),this.gl.enableVertexAttribArray(e),this.gl.vertexAttribPointer(e,2,this.gl.FLOAT,!1,0,0)});a(this,"setupUniforms",()=>{let e={u_time:this.gl.getUniformLocation(this.program,"u_time"),u_pixelRatio:this.gl.getUniformLocation(this.program,"u_pixelRatio"),u_resolution:this.gl.getUniformLocation(this.program,"u_resolution")};Object.entries(this.providedUniforms).forEach(([o,t])=>{if(e[o]=this.gl.getUniformLocation(this.program,o),t instanceof HTMLImageElement){let r=`${o}AspectRatio`;e[r]=this.gl.getUniformLocation(this.program,r)}}),this.uniformLocations=e});a(this,"renderScale",1);a(this,"parentWidth",0);a(this,"parentHeight",0);a(this,"parentDevicePixelWidth",0);a(this,"parentDevicePixelHeight",0);a(this,"devicePixelsSupported",!1);a(this,"resizeObserver",null);a(this,"setupResizeObserver",()=>{this.resizeObserver=new ResizeObserver(([e])=>{if(e?.borderBoxSize[0]){let o=e.devicePixelContentBoxSize?.[0];o!==void 0&&(this.devicePixelsSupported=!0,this.parentDevicePixelWidth=o.inlineSize,this.parentDevicePixelHeight=o.blockSize),this.parentWidth=e.borderBoxSize[0].inlineSize,this.parentHeight=e.borderBoxSize[0].blockSize}this.handleResize()}),this.resizeObserver.observe(this.parentElement)});a(this,"handleVisualViewportChange",()=>{this.resizeObserver?.disconnect(),this.setupResizeObserver()});a(this,"handleResize",()=>{let e=0,o=0,t=Math.max(1,window.devicePixelRatio),r=visualViewport?.scale??1;if(this.devicePixelsSupported){let f=Math.max(1,this.minPixelRatio/t);e=this.parentDevicePixelWidth*f*r,o=this.parentDevicePixelHeight*f*r}else{let f=Math.max(t,this.minPixelRatio)*r;if(this.isSafari){let d=te();f*=Math.max(1,d)}e=Math.round(this.parentWidth)*f,o=Math.round(this.parentHeight)*f}let l=Math.sqrt(this.maxPixelCount)/Math.sqrt(e*o),s=Math.min(1,l),n=Math.round(e*s),i=Math.round(o*s),u=n/Math.round(this.parentWidth);(this.canvasElement.width!==n||this.canvasElement.height!==i||this.renderScale!==u)&&(this.renderScale=u,this.canvasElement.width=n,this.canvasElement.height=i,this.resolutionChanged=!0,this.gl.viewport(0,0,this.gl.canvas.width,this.gl.canvas.height),this.render(performance.now()))});a(this,"render",e=>{if(this.hasBeenDisposed)return;if(this.program===null){console.warn("Tried to render before program or gl was initialized");return}let o=e-this.lastRenderTime;this.lastRenderTime=e,this.currentSpeed!==0&&(this.currentFrame+=o*this.currentSpeed),this.gl.clear(this.gl.COLOR_BUFFER_BIT),this.gl.useProgram(this.program),this.gl.uniform1f(this.uniformLocations.u_time,this.currentFrame*.001),this.resolutionChanged&&(this.gl.uniform2f(this.uniformLocations.u_resolution,this.gl.canvas.width,this.gl.canvas.height),this.gl.uniform1f(this.uniformLocations.u_pixelRatio,this.renderScale),this.resolutionChanged=!1),this.gl.drawArrays(this.gl.TRIANGLES,0,6),this.currentSpeed!==0?this.requestRender():this.rafId=null});a(this,"requestRender",()=>{this.rafId!==null&&cancelAnimationFrame(this.rafId),this.rafId=requestAnimationFrame(this.render)});a(this,"setTextureUniform",(e,o)=>{if(!o.complete||o.naturalWidth===0)throw new Error(`Paper Shaders: image for uniform ${e} must be fully loaded`);let t=this.textures.get(e);t&&this.gl.deleteTexture(t),this.textureUnitMap.has(e)||this.textureUnitMap.set(e,this.textureUnitMap.size);let r=this.textureUnitMap.get(e);this.gl.activeTexture(this.gl.TEXTURE0+r);let l=this.gl.createTexture();this.gl.bindTexture(this.gl.TEXTURE_2D,l),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_S,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_T,this.gl.CLAMP_TO_EDGE),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MIN_FILTER,this.gl.LINEAR),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MAG_FILTER,this.gl.LINEAR),this.gl.texImage2D(this.gl.TEXTURE_2D,0,this.gl.RGBA,this.gl.RGBA,this.gl.UNSIGNED_BYTE,o),this.mipmaps.includes(e)&&(this.gl.generateMipmap(this.gl.TEXTURE_2D),this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MIN_FILTER,this.gl.LINEAR_MIPMAP_LINEAR));let s=this.gl.getError();if(s!==this.gl.NO_ERROR||l===null){console.error("Paper Shaders: WebGL error when uploading texture:",s);return}this.textures.set(e,l);let n=this.uniformLocations[e];if(n){this.gl.uniform1i(n,r);let i=`${e}AspectRatio`,u=this.uniformLocations[i];if(u){let f=o.naturalWidth/o.naturalHeight;this.gl.uniform1f(u,f)}}});a(this,"areUniformValuesEqual",(e,o)=>e===o?!0:Array.isArray(e)&&Array.isArray(o)&&e.length===o.length?e.every((t,r)=>this.areUniformValuesEqual(t,o[r])):!1);a(this,"setUniformValues",e=>{this.gl.useProgram(this.program),Object.entries(e).forEach(([o,t])=>{let r=t;if(t instanceof HTMLImageElement&&(r=`${t.src.slice(0,200)}|${t.naturalWidth}x${t.naturalHeight}`),this.areUniformValuesEqual(this.uniformCache[o],r))return;this.uniformCache[o]=r;let l=this.uniformLocations[o];if(!l){console.warn(`Uniform location for ${o} not found`);return}if(t instanceof HTMLImageElement)this.setTextureUniform(o,t);else if(Array.isArray(t)){let s=null,n=null;if(t[0]!==void 0&&Array.isArray(t[0])){let i=t[0].length;if(t.every(u=>u.length===i))s=t.flat(),n=i;else{console.warn(`All child arrays must be the same length for ${o}`);return}}else s=t,n=s.length;switch(n){case 2:this.gl.uniform2fv(l,s);break;case 3:this.gl.uniform3fv(l,s);break;case 4:this.gl.uniform4fv(l,s);break;case 9:this.gl.uniformMatrix3fv(l,!1,s);break;case 16:this.gl.uniformMatrix4fv(l,!1,s);break;default:console.warn(`Unsupported uniform array length: ${n}`)}}else typeof t=="number"?this.gl.uniform1f(l,t):typeof t=="boolean"?this.gl.uniform1i(l,t?1:0):console.warn(`Unsupported uniform type for ${o}: ${typeof t}`)})});a(this,"getCurrentFrame",()=>this.currentFrame);a(this,"setFrame",e=>{this.currentFrame=e,this.lastRenderTime=performance.now(),this.render(performance.now())});a(this,"setSpeed",(e=1)=>{this.speed=e,this.setCurrentSpeed(document.hidden?0:e)});a(this,"setCurrentSpeed",e=>{this.currentSpeed=e,this.rafId===null&&e!==0&&(this.lastRenderTime=performance.now(),this.rafId=requestAnimationFrame(this.render)),this.rafId!==null&&e===0&&(cancelAnimationFrame(this.rafId),this.rafId=null)});a(this,"setMaxPixelCount",(e=z)=>{this.maxPixelCount=e,this.handleResize()});a(this,"setMinPixelRatio",(e=2)=>{this.minPixelRatio=e,this.handleResize()});a(this,"setUniforms",e=>{this.setUniformValues(e),this.providedUniforms={...this.providedUniforms,...e},this.render(performance.now())});a(this,"handleDocumentVisibilityChange",()=>{this.setCurrentSpeed(document.hidden?0:this.speed)});a(this,"dispose",()=>{this.hasBeenDisposed=!0,this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.gl&&this.program&&(this.textures.forEach(e=>{this.gl.deleteTexture(e)}),this.textures.clear(),this.gl.deleteProgram(this.program),this.program=null,this.gl.bindBuffer(this.gl.ARRAY_BUFFER,null),this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER,null),this.gl.bindRenderbuffer(this.gl.RENDERBUFFER,null),this.gl.bindFramebuffer(this.gl.FRAMEBUFFER,null),this.gl.getError()),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),visualViewport?.removeEventListener("resize",this.handleVisualViewportChange),document.removeEventListener("visibilitychange",this.handleDocumentVisibilityChange),this.uniformLocations={},this.canvasElement.remove(),delete this.parentElement.paperShaderMount});if(e instanceof HTMLElement)this.parentElement=e;else throw new Error("Paper Shaders: parent element must be an HTMLElement");if(!document.querySelector("style[data-paper-shader]")){let x=document.createElement("style");x.innerHTML=ee,x.setAttribute("data-paper-shader",""),document.head.prepend(x)}let f=document.createElement("canvas");this.canvasElement=f,this.parentElement.prepend(f),this.fragmentShader=o,this.providedUniforms=t,this.mipmaps=u,this.currentFrame=s,this.minPixelRatio=n,this.maxPixelCount=i;let d=f.getContext("webgl2",r);if(!d)throw new Error("Paper Shaders: WebGL is not supported in this browser");this.gl=d,this.initProgram(),this.setupPositionAttribute(),this.setupUniforms(),this.setUniformValues(this.providedUniforms),this.setupResizeObserver(),visualViewport?.addEventListener("resize",this.handleVisualViewportChange),this.setSpeed(l),this.parentElement.setAttribute("data-paper-shader",""),this.parentElement.paperShaderMount=this,document.addEventListener("visibilitychange",this.handleDocumentVisibilityChange)}};function O(e,o,t){let r=e.createShader(o);return r?(e.shaderSource(r,t),e.compileShader(r),e.getShaderParameter(r,e.COMPILE_STATUS)?r:(console.error("An error occurred compiling the shaders: "+e.getShaderInfoLog(r)),e.deleteShader(r),null)):null}function $(e,o,t){let r=e.getShaderPrecisionFormat(e.FRAGMENT_SHADER,e.MEDIUM_FLOAT),l=r?r.precision:null;l&&l<23&&(o=o.replace(/precision\s+(lowp|mediump)\s+float;/g,"precision highp float;"),t=t.replace(/precision\s+(lowp|mediump)\s+float/g,"precision highp float").replace(/\b(uniform|varying|attribute)\s+(lowp|mediump)\s+(\w+)/g,"$1 highp $3"));let s=O(e,e.VERTEX_SHADER,o),n=O(e,e.FRAGMENT_SHADER,t);if(!s||!n)return null;let i=e.createProgram();return i?(e.attachShader(i,s),e.attachShader(i,n),e.linkProgram(i),e.getProgramParameter(i,e.LINK_STATUS)?(e.detachShader(i,s),e.detachShader(i,n),e.deleteShader(s),e.deleteShader(n),i):(console.error("Unable to initialize the shader program: "+e.getProgramInfoLog(i)),e.deleteProgram(i),e.deleteShader(s),e.deleteShader(n),null)):null}var ee=`@layer paper-shaders {
152
152
  :where([data-paper-shader]) {
153
153
  isolation: isolate;
154
154
  position: relative;
@@ -165,10 +165,10 @@ void main() {
165
165
  corner-shape: inherit;
166
166
  }
167
167
  }
168
- }`;function oe(){let e=navigator.userAgent.toLowerCase();return e.includes("safari")&&!e.includes("chrome")&&!e.includes("android")}function te(){let e=visualViewport?.scale??1,o=visualViewport?.width??window.innerWidth,t=window.innerWidth-document.documentElement.clientWidth,i=e*o+t,n=outerWidth/i,s=Math.round(100*n);return s%5===0?s/100:s===33?1/3:s===67?2/3:s===133?4/3:n}var c=`
168
+ }`;function oe(){let e=navigator.userAgent.toLowerCase();return e.includes("safari")&&!e.includes("chrome")&&!e.includes("android")}function te(){let e=visualViewport?.scale??1,o=visualViewport?.width??window.innerWidth,t=window.innerWidth-document.documentElement.clientWidth,r=e*o+t,l=outerWidth/r,s=Math.round(100*l);return s%5===0?s/100:s===33?1/3:s===67?2/3:s===133?4/3:l}var c=`
169
169
  #define TWO_PI 6.28318530718
170
170
  #define PI 3.14159265358979323846
171
- `,f=`
171
+ `,m=`
172
172
  vec2 rotate(vec2 uv, float th) {
173
173
  return mat2(cos(th), sin(th), -sin(th), cos(th)) * uv;
174
174
  }
@@ -279,7 +279,7 @@ in vec2 v_objectUV;
279
279
  out vec4 fragColor;
280
280
 
281
281
  ${c}
282
- ${f}
282
+ ${m}
283
283
  ${g}
284
284
 
285
285
  float valueNoise(vec2 st) {
@@ -505,7 +505,7 @@ in vec2 v_patternUV;
505
505
 
506
506
  out vec4 fragColor;
507
507
 
508
- ${f}
508
+ ${m}
509
509
 
510
510
  float neuroShape(vec2 uv, float t) {
511
511
  vec2 sine_acc = vec2(0.);
@@ -575,7 +575,7 @@ in vec2 v_patternUV;
575
575
  out vec4 fragColor;
576
576
 
577
577
  ${c}
578
- ${f}
578
+ ${m}
579
579
  ${_}
580
580
  ${E}
581
581
 
@@ -1293,7 +1293,7 @@ in vec2 v_patternUV;
1293
1293
  out vec4 fragColor;
1294
1294
 
1295
1295
  ${c}
1296
- ${f}
1296
+ ${m}
1297
1297
  float randomG(vec2 p) {
1298
1298
  vec2 uv = floor(p) / 100. + .5;
1299
1299
  return texture(u_noiseTexture, fract(uv)).g;
@@ -1404,7 +1404,7 @@ in vec2 v_objectUV;
1404
1404
  out vec4 fragColor;
1405
1405
 
1406
1406
  ${c}
1407
- ${f}
1407
+ ${m}
1408
1408
  ${_}
1409
1409
  float valueNoise(vec2 st) {
1410
1410
  vec2 i = floor(st);
@@ -1588,7 +1588,7 @@ out vec4 fragColor;
1588
1588
 
1589
1589
  ${c}
1590
1590
  ${h}
1591
- ${f}
1591
+ ${m}
1592
1592
 
1593
1593
  void main() {
1594
1594
  vec2 shape_uv = v_objectUV;
@@ -1930,7 +1930,7 @@ out vec4 fragColor;
1930
1930
 
1931
1931
  ${c}
1932
1932
  ${h}
1933
- ${f}
1933
+ ${m}
1934
1934
  ${_}
1935
1935
 
1936
1936
  float valueNoiseR(vec2 st) {
@@ -2607,7 +2607,7 @@ in vec2 v_objectUV;
2607
2607
  out vec4 fragColor;
2608
2608
 
2609
2609
  ${c}
2610
- ${f}
2610
+ ${m}
2611
2611
  ${g}
2612
2612
 
2613
2613
  float valueNoise(vec2 st) {
@@ -2721,7 +2721,7 @@ in vec2 v_objectUV;
2721
2721
  out vec4 fragColor;
2722
2722
 
2723
2723
  ${c}
2724
- ${f}
2724
+ ${m}
2725
2725
  ${g}
2726
2726
 
2727
2727
  float valueNoise(vec2 st) {
@@ -2909,7 +2909,7 @@ float getUvFrame(vec2 uv) {
2909
2909
  }
2910
2910
 
2911
2911
  ${c}
2912
- ${f}
2912
+ ${m}
2913
2913
  ${_}
2914
2914
  float valueNoise(vec2 st) {
2915
2915
  vec2 i = floor(st);
@@ -3125,7 +3125,7 @@ in vec2 v_imageUV;
3125
3125
  out vec4 fragColor;
3126
3126
 
3127
3127
  ${c}
3128
- ${f}
3128
+ ${m}
3129
3129
  ${h}
3130
3130
 
3131
3131
  float getUvFrame(vec2 uv) {
@@ -3247,7 +3247,7 @@ in vec2 v_imageUV;
3247
3247
  out vec4 fragColor;
3248
3248
 
3249
3249
  ${c}
3250
- ${f}
3250
+ ${m}
3251
3251
  ${g}
3252
3252
 
3253
3253
  float valueNoise(vec2 st) {
@@ -4003,7 +4003,7 @@ in vec2 v_imageUV;
4003
4003
  out vec4 fragColor;
4004
4004
 
4005
4005
  ${c}
4006
- ${f}
4006
+ ${m}
4007
4007
  ${h}
4008
4008
 
4009
4009
  float getColorChanges(float c1, float c2, float stripe_p, vec3 w, float blur, float bump, float tint) {
@@ -4322,7 +4322,7 @@ in vec2 v_imageUV;
4322
4322
  out vec4 fragColor;
4323
4323
 
4324
4324
  ${c}
4325
- ${f}
4325
+ ${m}
4326
4326
  ${g}
4327
4327
 
4328
4328
  float valueNoise(vec2 st) {
@@ -4864,4 +4864,4 @@ void main() {
4864
4864
 
4865
4865
  fragColor = vec4(color, opacity);
4866
4866
  }
4867
- `;function A(e){if(Array.isArray(e))return e.length===4?e:e.length===3?[...e,1]:M;if(typeof e!="string")return M;let o,t,i,n=1;if(e.startsWith("#"))[o,t,i,n]=ie(e);else if(e.startsWith("rgb"))[o,t,i,n]=re(e);else if(e.startsWith("hsl"))[o,t,i,n]=le(se(e));else return console.error("Unsupported color format",e),M;return[C(o,0,1),C(t,0,1),C(i,0,1),C(n,0,1)]}function ie(e){e=e.replace(/^#/,""),e.length===3&&(e=e.split("").map(s=>s+s).join("")),e.length===6&&(e=e+"ff");let o=parseInt(e.slice(0,2),16)/255,t=parseInt(e.slice(2,4),16)/255,i=parseInt(e.slice(4,6),16)/255,n=parseInt(e.slice(6,8),16)/255;return[o,t,i,n]}function re(e){let o=e.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([0-9.]+))?\s*\)$/i);return o?[parseInt(o[1]??"0")/255,parseInt(o[2]??"0")/255,parseInt(o[3]??"0")/255,o[4]===void 0?1:parseFloat(o[4])]:[0,0,0,1]}function se(e){let o=e.match(/^hsla?\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*(?:,\s*([0-9.]+))?\s*\)$/i);return o?[parseInt(o[1]??"0"),parseInt(o[2]??"0"),parseInt(o[3]??"0"),o[4]===void 0?1:parseFloat(o[4])]:[0,0,0,1]}function le(e){let[o,t,i,n]=e,s=o/360,r=t/100,l=i/100,m,u,d;if(t===0)m=u=d=l;else{let x=(y,V,v)=>(v<0&&(v+=1),v>1&&(v-=1),v<.16666666666666666?y+(V-y)*6*v:v<.5?V:v<.6666666666666666?y+(V-y)*(.6666666666666666-v)*6:y),b=l<.5?l*(1+r):l+r-l*r,U=2*l-b;m=x(U,b,s+1/3),u=x(U,b,s),d=x(U,b,s-1/3)}return[m,u,d,n]}var C=(e,o,t)=>Math.min(Math.max(e,o),t),M=[0,0,0,1];function Qe(e,o){let t=B[o.shape]??B.warp,i=S[o.type]??S["2x2"],n={u_colorFront:A(o.colorFront),u_colorBack:A(o.colorBack),u_shape:t,u_type:i,u_pxSize:o.size,u_scale:o.scale,u_rotation:parseFloat(o.rotation)||180,u_originX:.5,u_originY:.5,u_worldWidth:0,u_worldHeight:0,u_fit:0,u_offsetX:0,u_offsetY:0},s=new X(e,J,n,{alpha:!0,premultipliedAlpha:!1},o.speed,0,2);return{updateParams(r){let l={};r.colorFront!==void 0&&(l.u_colorFront=A(r.colorFront)),r.colorBack!==void 0&&(l.u_colorBack=A(r.colorBack)),r.shape!==void 0&&(l.u_shape=B[r.shape]??t),r.type!==void 0&&(l.u_type=S[r.type]??i),r.size!==void 0&&(l.u_pxSize=r.size),r.scale!==void 0&&(l.u_scale=r.scale),r.rotation!==void 0&&(l.u_rotation=parseFloat(r.rotation)||180),r.speed!==void 0&&s.setSpeed(r.speed),Object.keys(l).length>0&&s.setUniforms(l)},destroy(){s.dispose()}}}export{Qe as createDitheringRenderer};
4867
+ `;function A(e){if(Array.isArray(e))return e.length===4?e:e.length===3?[...e,1]:M;if(typeof e!="string")return M;let o,t,r,l=1;if(e.startsWith("#"))[o,t,r,l]=ie(e);else if(e.startsWith("rgb"))[o,t,r,l]=re(e);else if(e.startsWith("hsl"))[o,t,r,l]=le(se(e));else return console.error("Unsupported color format",e),M;return[C(o,0,1),C(t,0,1),C(r,0,1),C(l,0,1)]}function ie(e){e=e.replace(/^#/,""),e.length===3&&(e=e.split("").map(s=>s+s).join("")),e.length===6&&(e=e+"ff");let o=parseInt(e.slice(0,2),16)/255,t=parseInt(e.slice(2,4),16)/255,r=parseInt(e.slice(4,6),16)/255,l=parseInt(e.slice(6,8),16)/255;return[o,t,r,l]}function re(e){let o=e.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([0-9.]+))?\s*\)$/i);return o?[parseInt(o[1]??"0")/255,parseInt(o[2]??"0")/255,parseInt(o[3]??"0")/255,o[4]===void 0?1:parseFloat(o[4])]:[0,0,0,1]}function se(e){let o=e.match(/^hsla?\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*(?:,\s*([0-9.]+))?\s*\)$/i);return o?[parseInt(o[1]??"0"),parseInt(o[2]??"0"),parseInt(o[3]??"0"),o[4]===void 0?1:parseFloat(o[4])]:[0,0,0,1]}function le(e){let[o,t,r,l]=e,s=o/360,n=t/100,i=r/100,u,f,d;if(t===0)u=f=d=i;else{let x=(y,V,v)=>(v<0&&(v+=1),v>1&&(v-=1),v<.16666666666666666?y+(V-y)*6*v:v<.5?V:v<.6666666666666666?y+(V-y)*(.6666666666666666-v)*6:y),b=i<.5?i*(1+n):i+n-i*n,U=2*i-b;u=x(U,b,s+1/3),f=x(U,b,s),d=x(U,b,s-1/3)}return[u,f,d,l]}var C=(e,o,t)=>Math.min(Math.max(e,o),t),M=[0,0,0,1];function Qe(e,o){let t=B[o.shape]??B.warp,r=S[o.type]??S["2x2"],l={u_colorFront:A(o.colorFront),u_colorBack:A(o.colorBack),u_shape:t,u_type:r,u_pxSize:o.size,u_scale:o.scale,u_rotation:parseFloat(o.rotation)||180,u_originX:.5,u_originY:.5,u_worldWidth:0,u_worldHeight:0,u_fit:0,u_offsetX:0,u_offsetY:0},s=new X(e,J,l,{alpha:!0,premultipliedAlpha:!1},o.speed,0,2),n={updateParams(i){let u={};i.colorFront!==void 0&&(u.u_colorFront=A(i.colorFront)),i.colorBack!==void 0&&(u.u_colorBack=A(i.colorBack)),i.shape!==void 0&&(u.u_shape=B[i.shape]??t),i.type!==void 0&&(u.u_type=S[i.type]??r),i.size!==void 0&&(u.u_pxSize=i.size),i.scale!==void 0&&(u.u_scale=i.scale),i.rotation!==void 0&&(u.u_rotation=parseFloat(i.rotation)||180),i.speed!==void 0&&s.setSpeed(i.speed),Object.keys(u).length>0&&s.setUniforms(u)},destroy(){s.dispose(),window.__BD_SHADER__===n&&(window.__BD_SHADER__=null)}};return window.__BD_SHADER__=n,n}export{Qe as createDitheringRenderer};
@@ -0,0 +1,91 @@
1
+ /**
2
+ * BlueDither Sidecar — Lightweight dev server for vanilla projects.
3
+ *
4
+ * Serves static files from the current directory and handles
5
+ * POST /__bluedither/commit to save tokens.json to disk.
6
+ *
7
+ * Usage: node bluedither/sidecar.js [port]
8
+ * Default port: 3456
9
+ */
10
+
11
+ import { createServer } from 'http';
12
+ import { readFileSync, writeFileSync, existsSync, statSync } from 'fs';
13
+ import { resolve, join, extname } from 'path';
14
+
15
+ const PORT = parseInt(process.argv[2] || '3456', 10);
16
+ const ROOT = process.cwd();
17
+
18
+ const MIME = {
19
+ '.html': 'text/html',
20
+ '.css': 'text/css',
21
+ '.js': 'application/javascript',
22
+ '.mjs': 'application/javascript',
23
+ '.json': 'application/json',
24
+ '.png': 'image/png',
25
+ '.jpg': 'image/jpeg',
26
+ '.jpeg': 'image/jpeg',
27
+ '.gif': 'image/gif',
28
+ '.svg': 'image/svg+xml',
29
+ '.ico': 'image/x-icon',
30
+ '.woff': 'font/woff',
31
+ '.woff2': 'font/woff2',
32
+ '.ttf': 'font/ttf',
33
+ '.otf': 'font/otf',
34
+ };
35
+
36
+ function findTokensPath() {
37
+ const candidates = [
38
+ resolve(ROOT, 'bluedither', 'tokens.json'),
39
+ resolve(ROOT, 'tokens.json'),
40
+ ];
41
+ for (const p of candidates) {
42
+ if (existsSync(p)) return p;
43
+ }
44
+ return candidates[0]; // default to bluedither/tokens.json
45
+ }
46
+
47
+ const server = createServer((req, res) => {
48
+ // Handle commit endpoint
49
+ if (req.method === 'POST' && req.url === '/__bluedither/commit') {
50
+ let body = '';
51
+ req.on('data', (chunk) => { body += chunk; });
52
+ req.on('end', () => {
53
+ try {
54
+ JSON.parse(body); // validate JSON
55
+ const tokensPath = findTokensPath();
56
+ writeFileSync(tokensPath, body);
57
+ res.writeHead(200, { 'Content-Type': 'application/json' });
58
+ res.end(JSON.stringify({ ok: true, path: tokensPath }));
59
+ console.log(` Saved tokens to ${tokensPath}`);
60
+ } catch (e) {
61
+ res.writeHead(500, { 'Content-Type': 'application/json' });
62
+ res.end(JSON.stringify({ error: e.message }));
63
+ }
64
+ });
65
+ return;
66
+ }
67
+
68
+ // Serve static files
69
+ let filePath = join(ROOT, decodeURIComponent(req.url.split('?')[0]));
70
+ if (filePath.endsWith('/') || filePath === ROOT) {
71
+ filePath = join(filePath, 'index.html');
72
+ }
73
+
74
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) {
75
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
76
+ res.end('Not Found');
77
+ return;
78
+ }
79
+
80
+ const ext = extname(filePath).toLowerCase();
81
+ const mime = MIME[ext] || 'application/octet-stream';
82
+ const content = readFileSync(filePath);
83
+ res.writeHead(200, { 'Content-Type': mime });
84
+ res.end(content);
85
+ });
86
+
87
+ server.listen(PORT, () => {
88
+ console.log(`\n BlueDither dev server running at http://localhost:${PORT}/\n`);
89
+ console.log(` Commit endpoint: POST /__bluedither/commit`);
90
+ console.log(` Serving files from: ${ROOT}\n`);
91
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluedither",
3
- "version": "1.0.24",
3
+ "version": "1.0.25",
4
4
  "description": "A bold, dithered-shader hero theme for Claude Code — skill + fine-tuner",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,124 +2,83 @@
2
2
 
3
3
  When the target project is **plain HTML/CSS/JS** (no framework detected), follow these patterns.
4
4
 
5
- ## Output Structure
6
-
7
- Generate a single `index.html` file containing:
8
- 1. Inline `<style>` with CSS custom properties from tokens + all `.bd-*` class rules
9
- 2. Semantic HTML matching `structure.json` exactly
10
- 3. `<script type="module">` at the end of `<body>` that:
11
- - Imports `bluedither-shader.js` (or the bundled version)
12
- - Initializes `ShaderMount` on `#bd-shader-parent`
5
+ ## Dev Server
13
6
 
14
- ## CSS Custom Properties
7
+ Vanilla projects use the BlueDither sidecar as their dev server. It serves static files AND handles the "Commit Changes" endpoint.
15
8
 
16
- Compute `clamp()` values from all `referencePx` and spacing tokens using:
17
- ```
18
- clamp(max*0.55 rem, px/designWidth*100 vw, px/16 rem)
9
+ ```bash
10
+ node bluedither/sidecar.js
19
11
  ```
20
12
 
21
- Emit all properties under `:root` using the `--bd-*` namespace. See `template/index.html` for the exact variable names.
13
+ This starts a server at `http://localhost:3456/`. The sidecar handles `POST /__bluedither/commit` to save tokens.json to disk.
22
14
 
23
- ## Google Fonts
15
+ ## Output Structure
24
16
 
25
- Include `<link>` tags for `typography.primaryFont` and `typography.secondaryFont` via Google Fonts CDN:
26
- ```html
27
- <link href="https://fonts.googleapis.com/css2?family=FONT_NAME:wght@400;700&display=swap" rel="stylesheet">
28
- ```
29
- URL-encode the font family name.
17
+ Generate a single `index.html` file containing:
18
+ 1. Inline `<style>` with CSS custom properties from tokens + all `.bd-*` class rules
19
+ 2. Semantic HTML matching `structure.json` exactly
20
+ 3. `<script type="module">` at the end of `<body>` that imports and initializes the shader
30
21
 
31
22
  ## CRITICAL: File Placement Rules
32
23
 
33
- **In project root (or `src/`):**
24
+ **In project root:**
34
25
  - `index.html` — the rendered page
35
- - `paper-shaders-bundle.js` — copied from `bluedither/shaders/`
26
+ - `bluedither-shader.js` — copied from `bluedither/shaders/bluedither-shader.js`
27
+ - `paper-shaders-bundle.js` — copied from `bluedither/shaders/paper-shaders-bundle.js`
36
28
 
37
- **In `public/bluedither/` (for Vite) or alongside `index.html` (for non-Vite):**
29
+ **In `bluedither/` (already placed by install):**
38
30
  - `bluedither-tuner.js` — tuner panel
39
31
  - `bluedither-tuner.css` — tuner styles
40
32
  - `bluedither-tuner-inject.js` — tuner loader
41
33
  - `tokens.json` — design tokens (read at runtime by tuner)
42
34
  - `tokens.defaults.json` — default tokens
43
- - `dev-middleware.js` — Vite commit endpoint
35
+ - `sidecar.js` — dev server
44
36
 
45
- Copy tuner assets:
37
+ Copy shader files to project root:
46
38
  ```bash
47
- mkdir -p public/bluedither
48
- cp bluedither/bluedither-tuner.js public/bluedither/
49
- cp bluedither/bluedither-tuner.css public/bluedither/
50
- cp bluedither/bluedither-tuner-inject.js public/bluedither/
51
- cp bluedither/tokens.json public/bluedither/
52
- cp bluedither/tokens.defaults.json public/bluedither/
53
- cp bluedither/dev-middleware.js public/bluedither/
39
+ cp bluedither/shaders/bluedither-shader.js ./bluedither-shader.js
40
+ cp bluedither/shaders/paper-shaders-bundle.js ./paper-shaders-bundle.js
54
41
  ```
55
42
 
56
43
  ## Shader Module
57
44
 
58
- Copy `bluedither/shaders/paper-shaders-bundle.js` to the project root (or `src/`).
59
-
60
- Import and initialize in a `<script type="module">` at the end of `<body>`. **CRITICAL:** You MUST create a `window.__BD_SHADER__` wrapper with an `updateParams()` method — the tuner calls `updateParams()` but ShaderMount only has `setUniforms()` and `setSpeed()`.
45
+ Import `createDitheringRenderer` from `bluedither-shader.js`. It auto-assigns `window.__BD_SHADER__` so the tuner can update shader params live.
61
46
 
62
47
  ```js
63
- import { ShaderMount, ditheringFragmentShader, DitheringShapes, DitheringTypes, getShaderColorFromString } from './paper-shaders-bundle.js';
48
+ import { createDitheringRenderer } from './bluedither-shader.js';
64
49
 
65
50
  const parent = document.getElementById('bd-shader-parent');
66
- const mount = new ShaderMount(parent, ditheringFragmentShader, {
67
- u_colorFront: getShaderColorFromString('#005A6A'),
68
- u_colorBack: getShaderColorFromString('#00000000'),
69
- u_shape: DitheringShapes.ripple,
70
- u_type: DitheringTypes['2x2'],
71
- u_pxSize: 4.2,
72
- u_scale: 1,
73
- u_rotation: 180,
74
- u_originX: 0.5, u_originY: 0.5,
75
- u_worldWidth: 0, u_worldHeight: 0,
76
- u_fit: 0, u_offsetX: 0, u_offsetY: 0,
77
- }, { alpha: true, premultipliedAlpha: false }, 0.41, 0, 2);
78
-
79
- // CRITICAL: wrapper with updateParams for the tuner
80
- window.__BD_SHADER__ = {
81
- updateParams(params) {
82
- const uniforms = {};
83
- if (params.colorFront !== undefined) uniforms.u_colorFront = getShaderColorFromString(params.colorFront);
84
- if (params.colorBack !== undefined) uniforms.u_colorBack = getShaderColorFromString(params.colorBack);
85
- if (params.shape !== undefined) uniforms.u_shape = DitheringShapes[params.shape] ?? DitheringShapes.warp;
86
- if (params.type !== undefined) uniforms.u_type = DitheringTypes[params.type] ?? DitheringTypes['2x2'];
87
- if (params.size !== undefined) uniforms.u_pxSize = params.size;
88
- if (params.scale !== undefined) uniforms.u_scale = params.scale;
89
- if (params.rotation !== undefined) uniforms.u_rotation = parseFloat(params.rotation) || 180;
90
- if (Object.keys(uniforms).length > 0) mount.setUniforms(uniforms);
91
- if (params.speed !== undefined) mount.setSpeed(params.speed);
92
- },
93
- };
51
+ createDitheringRenderer(parent, {
52
+ colorFront: '#005A6A',
53
+ colorBack: '#00000000',
54
+ shape: 'ripple',
55
+ type: '2x2',
56
+ size: 4.2,
57
+ scale: 1,
58
+ speed: 0.41,
59
+ rotation: 180,
60
+ });
94
61
  ```
95
62
 
96
- ## Tuner Loading (Dev-Only)
63
+ Replace the param values with the actual values from `tokens.json` shader + colors sections.
97
64
 
98
- Add this after the shader script to load the tuner in dev mode:
99
- ```html
100
- <script type="module">
101
- if (import.meta.env.DEV) {
102
- const s = document.createElement('script');
103
- s.src = '/bluedither/bluedither-tuner-inject.js';
104
- document.body.appendChild(s);
105
- }
106
- </script>
107
- ```
65
+ ## Tuner Loading
108
66
 
109
- For non-Vite vanilla projects (opened via file:// or a simple HTTP server), use localhost detection instead:
67
+ Load the tuner inject script. For vanilla projects served by the sidecar, use localhost detection:
110
68
  ```html
111
- <script type="module">
112
- if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
113
- const s = document.createElement('script');
114
- s.src = '/bluedither/bluedither-tuner-inject.js';
115
- document.body.appendChild(s);
116
- }
117
- </script>
69
+ <script src="./bluedither/bluedither-tuner-inject.js"></script>
118
70
  ```
119
71
 
72
+ This loads the tuner on every page load. The tuner applies saved tokens from `tokens.json` and shows the overlay panel.
73
+
120
74
  ## CSS Custom Properties
121
75
 
122
- The CSS variable names MUST match exactly what the tuner expects:
76
+ Compute `clamp()` values from all `referencePx` and spacing tokens using:
77
+ ```
78
+ clamp(max*0.55 rem, px/designWidth*100 vw, px/16 rem)
79
+ ```
80
+
81
+ The CSS variable names MUST match exactly what the tuner expects. **DO NOT use long names like `--bd-color-background` — use the short names below:**
123
82
 
124
83
  **Colors:** `--bd-bg`, `--bd-primary`, `--bd-text`, `--bd-cta-bg`, `--bd-cta-text`
125
84
  **Typography:** `--bd-font-primary`, `--bd-font-secondary`, `--bd-font-primary-weight`, `--bd-font-secondary-weight`, `--bd-headline-size`, `--bd-headline-lh`, `--bd-sub-size`, `--bd-sub-lh`, `--bd-sub-transform`, `--bd-logo-size`, `--bd-logo-lh`, `--bd-nav-size`, `--bd-nav-lh`, `--bd-cta-size`, `--bd-cta-lh`
@@ -128,13 +87,33 @@ The CSS variable names MUST match exactly what the tuner expects:
128
87
 
129
88
  Using different variable names will break live updates from the tuner.
130
89
 
90
+ ## CSS Rule References
91
+
92
+ In your CSS rules, reference the SHORT variable names:
93
+ ```css
94
+ .bd-root { background-color: var(--bd-bg); }
95
+ .bd-header { background-color: var(--bd-primary); }
96
+ .bd-logo { color: var(--bd-text); }
97
+ .bd-cta { background-color: var(--bd-cta-bg); }
98
+ .bd-cta-text { color: var(--bd-cta-text); }
99
+ .bd-subheadline { font-size: var(--bd-sub-size); line-height: var(--bd-sub-lh); text-transform: var(--bd-sub-transform); }
100
+ ```
101
+
102
+ ## Google Fonts
103
+
104
+ Include `<link>` tags for `typography.primaryFont` and `typography.secondaryFont` via Google Fonts CDN:
105
+ ```html
106
+ <link href="https://fonts.googleapis.com/css2?family=FONT_NAME:wght@400;700&display=swap" rel="stylesheet">
107
+ ```
108
+ URL-encode the font family name.
109
+
131
110
  ## File Output
132
111
 
133
112
  | File | Contents |
134
113
  |------|----------|
135
114
  | `index.html` | Complete rendered page |
136
- | `bluedither-shader.js` | Shader module (copied) |
137
- | `paper-shaders-bundle.js` | Shader library bundle (copied) |
115
+ | `bluedither-shader.js` | Shader wrapper module (copied from bluedither/shaders/) |
116
+ | `paper-shaders-bundle.js` | Shader library bundle (copied from bluedither/shaders/) |
138
117
 
139
118
  ## Content Handling
140
119
 
@@ -52,7 +52,7 @@ export function createDitheringRenderer(parentEl, params) {
52
52
  2, // minPixelRatio
53
53
  );
54
54
 
55
- return {
55
+ const wrapper = {
56
56
  updateParams(newParams) {
57
57
  const updatedUniforms = {};
58
58
 
@@ -87,6 +87,12 @@ export function createDitheringRenderer(parentEl, params) {
87
87
  },
88
88
  destroy() {
89
89
  mount.dispose();
90
+ if (window.__BD_SHADER__ === wrapper) window.__BD_SHADER__ = null;
90
91
  }
91
92
  };
93
+
94
+ // Auto-assign so the tuner can find it without extra wiring
95
+ window.__BD_SHADER__ = wrapper;
96
+
97
+ return wrapper;
92
98
  }
@@ -11,11 +11,11 @@
11
11
  <style>
12
12
  :root {
13
13
  /* Colors */
14
- --bd-color-background: {{colors.background}};
15
- --bd-color-primary: {{colors.primary}};
16
- --bd-color-text: {{colors.text}};
17
- --bd-color-cta-bg: {{colors.ctaBackground}};
18
- --bd-color-cta-text: {{colors.ctaText}};
14
+ --bd-bg: {{colors.background}};
15
+ --bd-primary: {{colors.primary}};
16
+ --bd-text: {{colors.text}};
17
+ --bd-cta-bg: {{colors.ctaBackground}};
18
+ --bd-cta-text: {{colors.ctaText}};
19
19
 
20
20
  /* Typography */
21
21
  --bd-font-primary: "{{typography.primaryFont}}", system-ui, sans-serif;
@@ -25,9 +25,9 @@
25
25
 
26
26
  --bd-headline-size: {{clamp.headline.fontSize}};
27
27
  --bd-headline-lh: {{clamp.headline.lineHeight}};
28
- --bd-subheadline-size: {{clamp.subHeadline.fontSize}};
29
- --bd-subheadline-lh: {{clamp.subHeadline.lineHeight}};
30
- --bd-subheadline-transform: {{typography.subHeadline.textTransform}};
28
+ --bd-sub-size: {{clamp.subHeadline.fontSize}};
29
+ --bd-sub-lh: {{clamp.subHeadline.lineHeight}};
30
+ --bd-sub-transform: {{typography.subHeadline.textTransform}};
31
31
  --bd-logo-size: {{clamp.logo.fontSize}};
32
32
  --bd-logo-lh: {{clamp.logo.lineHeight}};
33
33
  --bd-nav-size: {{clamp.navItem.fontSize}};
@@ -59,7 +59,7 @@
59
59
  flex-direction: column;
60
60
  width: 100%;
61
61
  min-height: 100vh;
62
- background-color: var(--bd-color-background);
62
+ background-color: var(--bd-bg);
63
63
  overflow: clip;
64
64
  }
65
65
 
@@ -73,7 +73,7 @@
73
73
  width: 100%;
74
74
  padding-inline: var(--bd-header-px);
75
75
  padding-block: var(--bd-header-py);
76
- background-color: var(--bd-color-primary);
76
+ background-color: var(--bd-primary);
77
77
  overflow: clip;
78
78
  flex-shrink: 0;
79
79
  }
@@ -90,7 +90,7 @@
90
90
  font-weight: var(--bd-font-primary-weight);
91
91
  font-size: var(--bd-logo-size);
92
92
  line-height: var(--bd-logo-lh);
93
- color: var(--bd-color-text);
93
+ color: var(--bd-text);
94
94
  white-space: pre;
95
95
  flex-shrink: 0;
96
96
  }
@@ -108,7 +108,7 @@
108
108
  font-weight: var(--bd-font-primary-weight);
109
109
  font-size: var(--bd-nav-size);
110
110
  line-height: var(--bd-nav-lh);
111
- color: var(--bd-color-text);
111
+ color: var(--bd-text);
112
112
  white-space: pre;
113
113
  text-decoration: none;
114
114
  cursor: pointer;
@@ -117,7 +117,7 @@
117
117
  .bd-cta {
118
118
  display: flex;
119
119
  align-items: start;
120
- background-color: var(--bd-color-cta-bg);
120
+ background-color: var(--bd-cta-bg);
121
121
  border-radius: var(--bd-cta-radius);
122
122
  padding-inline: var(--bd-cta-px);
123
123
  padding-block: var(--bd-cta-py);
@@ -132,7 +132,7 @@
132
132
  font-weight: var(--bd-font-primary-weight);
133
133
  font-size: var(--bd-cta-size);
134
134
  line-height: var(--bd-cta-lh);
135
- color: var(--bd-color-cta-text);
135
+ color: var(--bd-cta-text);
136
136
  white-space: pre;
137
137
  }
138
138
 
@@ -188,7 +188,7 @@
188
188
  font-weight: var(--bd-font-primary-weight);
189
189
  font-size: var(--bd-headline-size);
190
190
  line-height: var(--bd-headline-lh);
191
- color: var(--bd-color-text);
191
+ color: var(--bd-text);
192
192
  white-space: pre;
193
193
  width: fit-content;
194
194
  flex-shrink: 0;
@@ -197,10 +197,10 @@
197
197
  .bd-subheadline {
198
198
  font-family: var(--bd-font-secondary);
199
199
  font-weight: var(--bd-font-secondary-weight);
200
- font-size: var(--bd-subheadline-size);
201
- line-height: var(--bd-subheadline-lh);
202
- text-transform: var(--bd-subheadline-transform);
203
- color: var(--bd-color-text);
200
+ font-size: var(--bd-sub-size);
201
+ line-height: var(--bd-sub-lh);
202
+ text-transform: var(--bd-sub-transform);
203
+ color: var(--bd-text);
204
204
  width: fit-content;
205
205
  flex-shrink: 0;
206
206
  }