[ Foro de PHP ]
Buenas tardes a todos,
Después de muchas horas e intentos con la IA para crear un plugin para Wordpress recurro a ustedes como expertos.
Consigo crear el plugin a mi gusto y funcional, pero la función principal es la aplicación de un filtro en concreto mediante sliders (detalle, contraste y brillo) a una imagen no consigo replicar exactamente igual a la que necesito, consigo algo parecido, es un filtro tipo comic de una app de android, la cual he extraído el código pero ni con esas.
¿Alguien me puede ayudar con el código?.
Por aquí copio y pego el código creado hasta ahora funcional (se activa median un Shortcode: [comic_filter]) y la app es "Comica" con el filtro "COMIC" a replicar
<?php
/*
Plugin Name: Comic Filter
Description: Conversor Artístico – Filtro cómic BN tipo COMICA (XDoG + umbral adaptativo). Vista previa y descarga con la misma estética.
Version: 3.5
Author: Borja
*/
if (!defined('ABSPATH')) exit;
add_shortcode('comic_filter', function () {
ob_start(); ?>
<div id="cf-app" style="text-align:center;max-width:960px;margin:20px auto;font-family:Arial,sans-serif;color:#222;">
<h1 class="cf-title">Conversor Artístico</h1>
<div style="margin:10px 0;">
<label for="cf-upload" class="cf-btn" style="cursor:pointer;display:inline-block;">Seleccionar archivo</label>
<input type="file" id="cf-upload" accept="image/*" style="display:none;">
<div id="cf-filename"></div>
</div>
<div id="cf-loading" style="display:none;color:#96ac60;font-weight:700;margin:10px 0;">Cargando archivo…</div>
<div id="cf-previews" style="display:flex;justify-content:center;align-items:flex-start;gap:24px;margin:16px 0 20px;flex-wrap:wrap;">
<div>
<p style="margin:6px 0;font-weight:600;">Original</p>
<canvas id="cf-original" style="max-width:400px;width:400px;height:auto;border:1px solid #ddd;border-radius:8px;background:#fff;"></canvas>
</div>
<div>
<p style="margin:6px 0;font-weight:600;">Vista Previa</p>
<canvas id="cf-comic" style="max-width:400px;width:400px;height:auto;border:1px solid #ddd;border-radius:8px;background:#fff;"></canvas>
</div>
</div>
<div id="cf-sliders" style="display:flex;flex-direction:column;gap:14px;max-width:320px;margin:0 auto 16px;text-align:left;">
<label> Nivel de detalle
<input type="range" id="cf-detail" min="2" max="20" value="10">
</label>
<label> Contraste
<input type="range" id="cf-contrast" min="-100" max="100" value="25">
</label>
<label> Brillo
<input type="range" id="cf-bright" min="-100" max="100" value="0">
</label>
</div>
<div style="margin-top:10px;">
<button id="cf-download" class="cf-btn">DESCARGAR</button>
</div>
<p style="font-size:12px;color:#666;margin-top:8px;">PNG, lado largo máx 2000 px (?300 ppp). Misma vista previa y descarga.</p>
</div>
<style>
.cf-title{color:#96ac60;margin-bottom:32px;font-size:90px;font-weight:900;line-height:1.1;text-align:center;}
@media (max-width:768px){.cf-title{font-size:50px}}
@media (max-width:480px){.cf-title{font-size:36px}}
.cf-btn{background:#96ac60;color:#fff;border:none;padding:14px 36px;font-size:18px;font-weight:800;border-radius:8px;cursor:pointer;transition:transform .06s ease,opacity .2s ease;}
.cf-btn:hover{opacity:.95}.cf-btn:active{transform:scale(.98)}
@media (max-width:768px){.cf-btn{padding:12px 28px;font-size:16px}}
@media (max-width:480px){.cf-btn{padding:10px 22px;font-size:15px}}
#cf-sliders input[type=range]{-webkit-appearance:none;width:100%;height:6px;background:#dfe3d3;border-radius:4px;outline:none;margin-top:4px}
#cf-sliders input[type=range]::-webkit-slider-runnable-track{height:6px;background:#96ac60;border-radius:4px}
#cf-sliders input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;border-radius:50%;background:#96ac60;border:2px solid #6b7c3f;margin-top:-6px;cursor:pointer}
#cf-sliders input[type=range]::-moz-range-track{height:6px;background:#96ac60;border-radius:4px}
#cf-sliders input[type=range]::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:#96ac60;border:2px solid #6b7c3f;cursor:pointer}
#cf-filename{margin-top:8px;font-size:14px;color:#555;text-align:center;word-break:break-word;white-space:normal}
</style>
<script>
const up=document.getElementById('cf-upload');
const nameEl=document.getElementById('cf-filename');
const loading=document.getElementById('cf-loading');
const oCan=document.getElementById('cf-original');
const cCan=document.getElementById('cf-comic');
const octx=oCan.getContext('2d',{willReadFrequently:true});
const cctx=cCan.getContext('2d',{willReadFrequently:true});
const detEl=document.getElementById('cf-detail');
const conEl=document.getElementById('cf-contrast');
const briEl=document.getElementById('cf-bright');
let img=new Image();
let refPreviewW=400; // ancho real de la vista previa (se actualiza)
up.addEventListener('change',e=>{
const f=e.target.files[0]; if(!f) return;
nameEl.textContent=f.name; loading.style.display='block';
const r=new FileReader();
r.onload=ev=>{ img.onload=()=>{loading.style.display='none'; drawBoth();}; img.src=ev.target.result; };
r.readAsDataURL(f);
});
function drawBoth(){
const maxW=400;
const scale=Math.min(maxW/img.width,1);
const W=Math.round(img.width*scale), H=Math.round(img.height*scale);
refPreviewW=W;
[oCan,cCan].forEach(c=>{c.width=W;c.height=H;});
octx.drawImage(img,0,0,W,H);
runComic(cctx,W,H,refPreviewW);
}
[detEl,conEl,briEl].forEach(el=>el.addEventListener('input',()=>{
if(!oCan.width) return;
runComic(cctx,cCan.width,cCan.height,refPreviewW);
}));
// -------- Motor tipo COMICA con escalado SUAVE (?) --------
function runComic(ctx,W,H,refW){
ctx.drawImage(oCan,0,0,W,H);
const src=ctx.getImageData(0,0,W,H); const s=src.data;
const detail=+detEl.value||10;
const contrast=+conEl.value||0;
const brightness=+briEl.value||0;
const gray=new Float32Array(W*H);
const cFactor=(259*(contrast+255))/(255*(259-contrast));
for(let i=0,g=0;i<s.length;i+=4,g++){
let v=0.299*s[i]+0.587*s[i+1]+0.114*s[i+2];
v=cFactor*(v-128)+128+brightness;
if(v<0)v=0; else if(v>255)v=255;
gray[g]=v/255;
}
const sizeScale=Math.sqrt( W / (refW||400) ); // <- suave
const sigmaBase=0.6 + (20-detail)*0.08; // 0.6..2.0
const sigma=Math.max(0.35, sigmaBase * sizeScale); // NO lineal
const k=1.6, p=1.0, phi=3.0;
const G1=gaussianBlur(gray,W,H,sigma);
const G2=gaussianBlur(gray,W,H,sigma*k);
const xdog=new Float32Array(W*H);
for(let i=0;i<xdog.length;i++){
const dog=G1[i]-p*G2[i];
xdog[i]=(gray[i]+phi*dog);
}
normalize01(xdog);
const T=0.75 - (detail-10)*0.01; // fijo (no escala por tamaño)
const line=new Uint8Array(W*H);
for(let i=0;i<line.length;i++) line[i]=(xdog[i]<T)?0:255;
const rBase=Math.max(2,Math.round(6+(20-detail)));
const r=Math.max(1, Math.round(rBase * sizeScale)); // ?-escala
const mean=boxBlurU8F(gray,W,H,r);
const C=0.06 + (20-detail)*0.01; // fijo
const regions=new Uint8Array(W*H);
for(let i=0;i<regions.length;i++) regions[i]=(gray[i]<(mean[i]-C))?0:255;
const passes=1; // NO escalar: evita “manchas” en alta
dilateBW(line,W,H,passes);
const out=ctx.createImageData(W,H); const d=out.data;
for(let i=0,pi=0;i<d.length;i+=4,pi++){
const v=(line[pi]===0 || regions[pi]===0)?0:255;
d[i]=d[i+1]=d[i+2]=v; d[i+3]=255;
}
ctx.putImageData(out,0,0);
}
// ---- utilidades ----
function gaussianBlur(src,W,H,sigma){
const r=Math.max(1,Math.ceil(sigma*3));
const k=new Float32Array(2*r+1); const s2=sigma*sigma*2; let sum=0;
for(let i=-r,j=0;i<=r;i++,j++){ const v=Math.exp(-(i*i)/s2); k[j]=v; sum+=v; }
for(let j=0;j<k.length;j++) k[j]/=sum;
const tmp=new Float32Array(W*H);
for(let y=0;y<H;y++){
for(let x=0;x<W;x++){
let acc=0; for(let i=-r;i<=r;i++){ const xx=clamp(x+i,0,W-1); acc+=src[y*W+xx]*k[i+r]; }
tmp[y*W+x]=acc;
}
}
const out=new Float32Array(W*H);
for(let x=0;x<W;x++){
for(let y=0;y<H;y++){
let acc=0; for(let i=-r;i<=r;i++){ const yy=clamp(y+i,0,H-1); acc+=tmp[yy*W+x]*k[i+r]; }
out[y*W+x]=acc;
}
}
return out;
}
function boxBlurU8F(srcF,W,H,r){
const out=new Float32Array(W*H);
if(r<=0){out.set(srcF);return out;}
const wnd=2*r+1, tmp=new Float32Array(W*H);
for(let y=0;y<H;y++){
let acc=0; for(let i=-r;i<=r;i++) acc+=srcF[y*W+clamp(i,0,W-1)];
tmp[y*W]=acc/wnd;
for(let x=1;x<W;x++){
acc+=srcF[y*W+clamp(x+r,0,W-1)]-srcF[y*W+clamp(x-r-1,0,W-1)];
tmp[y*W+x]=acc/wnd;
}
}
for(let x=0;x<W;x++){
let acc=0; for(let i=-r;i<=r;i++) acc+=tmp[clamp(i,0,H-1)*W+x];
out[x]=acc/wnd;
for(let y=1;y<H;y++){
acc+=tmp[clamp(y+r,0,H-1)*W+x]-tmp[clamp(y-r-1,0,H-1)*W+x];
out[y*W+x]=acc/wnd;
}
}
return out;
}
function dilateBW(img,W,H,passes){
if(passes<=0) return;
const off=[-W-1,-W,-W+1,-1,1,W-1,W,W+1];
const buf=img.slice();
for(let p=0;p<passes;p++){
for(let y=1;y<H-1;y++){
for(let x=1;x<W-1;x++){
const i=y*W+x;
if(buf[i]===0){img[i]=0;continue;}
for(let k=0;k<8;k++){ if(buf[i+off[k]]===0){img[i]=0;break;} }
}
}
buf.set(img);
}
}
const clamp=(v,a,b)=> v<a?a:(v>b?b:v);
function normalize01(a){let mn=Infinity,mx=-Infinity;for(let i=0;i<a.length;i++){if(a[i]<mn)mn=a[i];if(a[i]>mx)mx=a[i];}const d=(mx-mn)||1;for(let i=0;i<a.length;i++)a[i]=(a[i]-mn)/d;}
// -------- Descarga: misma tubería con escalado SUAVE --------
document.getElementById('cf-download').addEventListener('click',()=>{
if(!oCan.width) return;
const maxSide=2000;
const scale=Math.min(1, maxSide/Math.max(img.width,img.height));
const W=Math.round(img.width*scale), H=Math.round(img.height*scale);
const big=document.createElement('canvas'); big.width=W; big.height=H;
const bctx=big.getContext('2d',{willReadFrequently:true});
bctx.drawImage(img,0,0,W,H);
// Usamos la MISMA función con refPreviewW para escalar como ?
// Para ello, copiamos el original escalado a un canvas "original" temporal
const tmp=document.createElement('canvas'); tmp.width=W; tmp.height=H;
tmp.getContext('2d').drawImage(img,0,0,W,H);
// Procesamos sobre bctx reutilizando runComic internamente:
// Implementación inline (idéntica a runComic) para evitar dependencia de oCan
const src=bctx.getImageData(0,0,W,H); const s=src.data;
const detail=+detEl.value||10, contrast=+conEl.value||0, brightness=+briEl.value||0;
const gray=new Float32Array(W*H);
const cFactor=(259*(contrast+255))/(255*(259-contrast));
for(let i=0,g=0;i<s.length;i+=4,g++){
let v=0.299*s[i]+0.587*s[i+1]+0.114*s[i+2];
v=cFactor*(v-128)+128+brightness;
if(v<0)v=0; else if(v>255)v=255;
gray[g]=v/255;
}
const sizeScale=Math.sqrt( W / (refPreviewW||400) );
const sigmaBase=0.6+(20-detail)*0.08;
const sigma=Math.max(0.35, sigmaBase*sizeScale);
const k=1.6, p=1.0, phi=3.0;
const G1=gaussianBlur(gray,W,H,sigma);
const G2=gaussianBlur(gray,W,H,sigma*k);
const xdog=new Float32Array(W*H);
for(let i=0;i<xdog.length;i++){ const dog=G1[i]-p*G2[i]; xdog[i]=(gray[i]+phi*dog); }
normalize01(xdog);
const T=0.75 - (detail-10)*0.01;
const line=new Uint8Array(W*H);
for(let i=0;i<line.length;i++) line[i]=(xdog[i]<T)?0:255;
const rBase=Math.max(2,Math.round(6+(20-detail)));
const r=Math.max(1, Math.round(rBase * sizeScale));
const mean=boxBlurU8F(gray,W,H,r);
const C=0.06 + (20-detail)*0.01;
const regions=new Uint8Array(W*H);
for(let i=0;i<regions.length;i++) regions[i]=(gray[i]<(mean[i]-C))?0:255;
const passes=1; // fijo
dilateBW(line,W,H,passes);
const out=bctx.createImageData(W,H); const d=out.data;
for(let i=0,pi=0;i<d.length;i+=4,pi++){
const v=(line[pi]===0 || regions[pi]===0)?0:255;
d[i]=d[i+1]=d[i+2]=v; d[i+3]=255;
}
bctx.putImageData(out,0,0);
const a=document.createElement('a');
a.download='comic.png';
a.href=big.toDataURL('image/png');
a.click();
});
</script>
<?php
return ob_get_clean();
});
Gracias