paradimg
Version:
Browser-side image URL modifiers and canvas effects.
2 lines • 7.76 kB
JavaScript
/*! paradimg v0.1.0 | (c) 2026 Rémino Rem <https://remino.net/> | ISC Licence */
(function(){var e=e=>{if(!e)return[];let t=new URL(e,window.location.href).hash.slice(1);return t?(t.startsWith(`?`)?t.slice(1):t).split(`&`).map(e=>{if(!e)return null;let t=e.indexOf(`=`),n=t!==-1,r=decodeURIComponent((n?e.slice(0,t):e).trim()).trim();return r?{keyword:r,rawValue:n?decodeURIComponent(e.slice(t+1).trim()):null}:null}).filter(Boolean):[]},t=e=>{let t=new URL(e,window.location.href);return t.hash=``,t.href},n=e=>new URL(e,window.location.href).hash,r=(e,n)=>{if(!n)return t(e);let r=new URL(e,window.location.href);return r.hash=n.slice(1),r.href},i=e=>{e.getAttribute(`width`)||e.setAttribute(`width`,e.naturalWidth),e.getAttribute(`height`)||e.setAttribute(`height`,e.naturalHeight)},a=class{constructor(e=[],t=`img[src*="#?"]`){this.selector=t,this.plugins=new Map;for(let t of e)this.register(t)}register(e){if(!e||!e.keyword)throw Error(`ImageProcessor plugins must define a keyword`);this.plugins.set(e.keyword,e)}getPlugin(e){return this.plugins.get(e)}async processImage(t){let a=t.dataset.imageProcessorOriginalSrc||t.src,o=n(a),s=e(a),c=s.map(e=>{let t=this.getPlugin(e.keyword);if(!t)return null;let n=t.formatValue?t.formatValue(e.rawValue,{flag:e,originalSrc:a}):e.rawValue;return{...e,plugin:t,value:n}}).filter(Boolean);if(!c.length)return!1;t.setAttribute(`data-image-processor-original-src`,a);let l=a;for(let e of c){t.src=l,await t.decode();let n=await e.plugin.process(t,{currentSrc:l,flag:e,flags:s,originalSrc:a,plugin:e.plugin,rawValue:e.rawValue,value:e.value,processor:this});n&&(l=r(n,o))}t.setAttribute(`data-image-processor-processed-src`,l),t.src=l,await t.decode(),i(t),t.classList.add(`image-processor--processed`);for(let e of c){let{plugin:n}=e;n.processedClass&&t.classList.add(n.processedClass),n.afterApply&&await n.afterApply(t,{currentSrc:l,flag:e,flags:s,originalSrc:a,plugin:n,rawValue:e.rawValue,value:e.value,processor:this})}return!0}enableToggle(e){e.dataset.imageProcessorToggleBound!==`1`&&(e.dataset.imageProcessorToggleBound=`1`,e.addEventListener(`click`,()=>{let t=e.dataset.imageProcessorOriginalSrc,n=e.dataset.imageProcessorProcessedSrc;!t||!n||(e.src=e.src===t?n:t)}))}async processImages(){let t=document.querySelectorAll(this.selector);for(let n of t)n.dataset.imageProcessorOriginalSrc&&(n.src=n.dataset.imageProcessorOriginalSrc),e(n.dataset.imageProcessorOriginalSrc||n.src).some(e=>this.getPlugin(e.keyword))&&await this.processImage(n)}},o=e=>new Promise((t,n)=>{e.toBlob(e=>{e?t(URL.createObjectURL(e)):n(Error(`Failed to convert canvas to blob`))},`image/png`)}),s=(e,t)=>{if(!e)return t;let n=e.trim().toLowerCase(),r=Number.parseFloat(n);return Number.isNaN(r)?t:r},c=async(e,t)=>{let n=e.naturalWidth,r=e.naturalHeight,i=document.createElement(`canvas`);i.width=n,i.height=r;let a=i.getContext(`2d`);a.drawImage(e,0,0,n,r);let s=a.getImageData(0,0,n,r),c=s.data;for(let e=0;e<c.length;e+=4){let[n,r,i]=t(c[e],c[e+1],c[e+2]);c[e]=n,c[e+1]=r,c[e+2]=i}return a.putImageData(s,0,0),o(i)},l={keyword:`brightness`,processedClass:`brightness--processed`,formatValue:e=>({factor:s(e,1.15)}),async process(e,{value:t}){return c(e,(e,n,r)=>[Math.min(255,Math.max(0,Math.round(e*t.factor))),Math.min(255,Math.max(0,Math.round(n*t.factor))),Math.min(255,Math.max(0,Math.round(r*t.factor)))])}},u={keyword:`contrast`,processedClass:`contrast--processed`,formatValue:e=>({factor:s(e,1.2)}),async process(e,{value:t}){let n=t.factor;return c(e,(e,t,r)=>[Math.min(255,Math.max(0,Math.round((e-128)*n+128))),Math.min(255,Math.max(0,Math.round((t-128)*n+128))),Math.min(255,Math.max(0,Math.round((r-128)*n+128)))])}},d=e=>new Promise((t,n)=>{e.toBlob(e=>{e?t(URL.createObjectURL(e)):n(Error(`Failed to convert canvas to blob`))},`image/png`)}),f=e=>{let t=e?.toLowerCase();return t===`invert`?{mode:`invert`}:t===`threshold`||t===`mono`?{mode:`threshold`}:{mode:`grayscale`}},p=async(e,t=`grayscale`)=>{let n=e.naturalWidth,r=e.naturalHeight,i=document.createElement(`canvas`);i.width=n,i.height=r;let a=i.getContext(`2d`);a.drawImage(e,0,0,n,r);let o=a.getImageData(0,0,n,r),s=o.data;for(let e=0;e<s.length;e+=4){let n=s[e],r=s[e+1],i=s[e+2],a=Math.round(.299*n+.587*r+.114*i);t===`invert`&&(a=255-a),t===`threshold`&&(a=a>=128?255:0),s[e]=a,s[e+1]=a,s[e+2]=a}return a.putImageData(o,0,0),d(i)},m={keyword:`bw`,processedClass:`bw--processed`,formatValue:f,async process(e,{value:t}){return p(e,t.mode)}},h=[[0,2],[3,1]],g=[[0,8,2,10],[12,4,14,6],[3,11,1,9],[15,7,13,5]],_=[[0,48,12,60,3,51,15,63],[32,16,44,28,35,19,47,31],[8,56,4,52,11,59,7,55],[40,24,36,20,43,27,39,23],[2,50,14,62,1,49,13,61],[34,18,46,30,33,17,45,29],[10,58,6,54,9,57,5,53],[42,26,38,22,41,25,37,21]],v={"2x2":h,"4x4":g,"8x8":_,"16x16":[[0,128,32,160,8,136,40,168,2,130,34,162,10,138,42,170],[192,64,224,96,200,72,232,104,194,66,226,98,202,74,234,106],[48,176,16,144,56,184,24,152,50,178,18,146,58,186,26,154],[240,112,208,80,248,120,216,88,242,114,210,82,250,122,218,90],[12,140,44,172,4,132,36,164,14,142,46,174,6,134,38,166],[204,76,236,108,196,68,228,100,206,78,238,110,198,70,230,102],[60,188,28,156,52,180,20,148,62,190,30,158,54,182,22,150],[252,124,220,92,244,116,212,84,254,126,222,94,246,118,214,86],[3,131,35,163,11,139,43,171,1,129,33,161,9,137,41,169],[195,67,227,99,203,75,235,107,193,65,225,97,201,73,233,105],[51,179,19,147,59,187,27,155,49,177,17,145,57,185,25,153],[243,115,211,83,251,123,219,91,241,113,209,81,249,121,217,89],[15,143,47,175,7,135,39,167,13,141,45,173,5,133,37,165],[207,79,239,111,199,71,231,103,205,77,237,109,197,69,229,101],[63,191,31,159,55,183,23,151,61,189,29,157,53,181,21,149],[255,127,223,95,247,119,215,87,253,125,221,93,245,117,213,85]],none:null},y=_,b=8,x=e=>new Promise((t,n)=>{e.toBlob(e=>{e?t(URL.createObjectURL(e)):n(Error(`Failed to convert canvas to blob`))},`image/png`)}),S=e=>{let t={matrix:y,levels:b,bw:!1};if(!e)return t;for(let n of e.split(`,`).map(e=>e.trim())){if(!n)continue;let e=n.toLowerCase();if(e===`bw`){t.bw=!0;continue}if(/^[248]c$/.test(e)&&!t.bw){t.levels=Number(e[0]);continue}let r=v[e];r!==void 0&&(t.matrix=r)}return t.bw&&(t.levels=2),t},C=async(e,t=1,n=_,r=8,i=!1)=>{let a=Math.floor(e.naturalWidth*t),o=Math.floor(e.naturalHeight*t),s=document.createElement(`canvas`);s.width=a,s.height=o;let c=s.getContext(`2d`);c.drawImage(e,0,0,a,o);let l=c.getImageData(0,0,a,o),u=l.data,d=255/(r-1);for(let e=0,t=0;t<o;t++)for(let r=0;r<a;r++,e+=4){let a=n?(n[t%n.length][r%n[0].length]/(n.length*n[0].length)-.5)*d:0;if(i){let t=Math.round(.299*u[e]+.587*u[e+1]+.114*u[e+2]),n=Math.min(255,Math.max(0,t+a)),r=Math.round(n/d)*d;u[e]=r,u[e+1]=r,u[e+2]=r;continue}for(let t=0;t<3;t++){let n=u[e+t]+a;n=Math.min(255,Math.max(0,n)),u[e+t]=Math.round(n/d)*d}}return c.putImageData(l,0,0),x(s)},w={keyword:`dither`,processedClass:`dither--processed`,toggleable:!0,formatValue:S,async process(e,{value:t}){return C(e,1,t.matrix,t.levels,t.bw)},afterApply(e,{processor:t}){t.enableToggle(e)}},T=e=>new Promise((t,n)=>{e.toBlob(e=>{e?t(URL.createObjectURL(e)):n(Error(`Failed to convert canvas to blob`))},`image/png`)}),E=async(e,t,n=!0)=>{let r=Math.max(1,Math.floor(e.naturalWidth*t)),i=Math.max(1,Math.floor(e.naturalHeight*t)),a=document.createElement(`canvas`);a.width=r,a.height=i;let o=a.getContext(`2d`);return o.imageSmoothingEnabled=n,o.imageSmoothingQuality=n?`high`:`low`,o.drawImage(e,0,0,r,i),T(a)},D=[m,l,u,{keyword:`half`,processedClass:`half--processed`,async process(e){return E(e,.5)}},{keyword:`double`,processedClass:`double--processed`,async process(e){return E(e,2,!1)}},w],O=(e={})=>new a(e.plugins??D,e.selector),k=e=>O(e).processImages(),A=()=>k();document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,A,{once:!0}):A()})();