From bc5c4625fcd8b59b1ddab00427dc34bbe11643e7 Mon Sep 17 00:00:00 2001 From: CNCKitchen Date: Wed, 1 Apr 2026 10:59:23 +0200 Subject: [PATCH] texture smoothing on mac --- js/main.js | 83 ++++++++++++++++++++++++++++++++++++++++--- js/presetTextures.js | 22 ++++++------ textures/grid.png | Bin 0 -> 3251 bytes textures/stripes.jpg | Bin 48429 -> 0 bytes 4 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 textures/grid.png delete mode 100644 textures/stripes.jpg diff --git a/js/main.js b/js/main.js index 0f2d87b..74d5146 100644 --- a/js/main.js +++ b/js/main.js @@ -59,6 +59,83 @@ const settings = { useDisplacement: false, }; +// ── Canvas filter support (Safari / iOS WebView don't support ctx.filter) ──── +const CANVAS_FILTER_SUPPORTED = (() => { + try { + const ctx = document.createElement('canvas').getContext('2d'); + ctx.filter = 'blur(1px)'; + return ctx.filter === 'blur(1px)'; + } catch (e) { return false; } +})(); + +/** + * Box-blur one row of RGBA pixels (horizontal pass). + * Operates in-place reading from `src` and writing to `dst`. + */ +function _boxBlurH(src, dst, w, h, r) { + const iarr = 1 / (2 * r + 1); + for (let y = 0; y < h; y++) { + const row = y * w; + for (let ch = 0; ch < 4; ch++) { + let val = 0; + // Seed with left-edge pixel repeated r+1 times plus the first r pixels + for (let x = -r; x <= r; x++) val += src[(row + Math.max(0, Math.min(x, w - 1))) * 4 + ch]; + for (let x = 0; x < w; x++) { + val += src[(row + Math.min(x + r, w - 1)) * 4 + ch] + - src[(row + Math.max(x - r - 1, 0)) * 4 + ch]; + dst[(row + x) * 4 + ch] = Math.round(val * iarr); + } + } + } +} + +/** Box-blur one column of RGBA pixels (vertical pass). */ +function _boxBlurV(src, dst, w, h, r) { + const iarr = 1 / (2 * r + 1); + for (let x = 0; x < w; x++) { + for (let ch = 0; ch < 4; ch++) { + let val = 0; + for (let y = -r; y <= r; y++) val += src[(Math.max(0, Math.min(y, h - 1)) * w + x) * 4 + ch]; + for (let y = 0; y < h; y++) { + val += src[(Math.min(y + r, h - 1) * w + x) * 4 + ch] + - src[(Math.max(y - r - 1, 0) * w + x) * 4 + ch]; + dst[(y * w + x) * 4 + ch] = Math.round(val * iarr); + } + } + } +} + +/** + * Apply an approximate Gaussian blur (sigma px) to `canvas` in-place. + * Uses the native CSS filter on Chrome/Firefox; falls back to a 3-pass + * separable box blur for Safari / iOS WebKit. + */ +function blurCanvas(canvas, sigma) { + if (sigma <= 0) return; + if (CANVAS_FILTER_SUPPORTED) { + const tmp = document.createElement('canvas'); + tmp.width = canvas.width; tmp.height = canvas.height; + const tc = tmp.getContext('2d'); + tc.filter = `blur(${sigma}px)`; + tc.drawImage(canvas, 0, 0); + canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); + canvas.getContext('2d').drawImage(tmp, 0, 0); + } else { + // 3 passes of box blur ≈ Gaussian; radius r where r(r+1) ≈ sigma² + const r = Math.max(1, Math.round((Math.sqrt(4 * sigma * sigma + 1) - 1) / 2)); + const ctx = canvas.getContext('2d'); + const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const a = imgData.data; + const b = new Uint8ClampedArray(a.length); + const w = canvas.width, h = canvas.height; + for (let pass = 0; pass < 3; pass++) { + _boxBlurH(a, b, w, h, r); + _boxBlurV(b, a, w, h, r); + } + ctx.putImageData(imgData, 0, 0); + } +} + // ── Displacement preview state ──────────────────────────────────────────────── let dispPreviewGeometry = null; // subdivided geometry with smoothNormal attribute let dispPreviewBusy = false; // true while async subdivision is running @@ -1332,10 +1409,8 @@ function getEffectiveMapEntry() { const blurred = document.createElement('canvas'); blurred.width = width * 3; blurred.height = height * 3; - const bc = blurred.getContext('2d'); - bc.filter = `blur(${settings.textureSmoothing}px)`; - bc.drawImage(tiled, 0, 0); - bc.filter = 'none'; + blurred.getContext('2d').drawImage(tiled, 0, 0); + blurCanvas(blurred, settings.textureSmoothing); const offscreen = document.createElement('canvas'); offscreen.width = width; offscreen.height = height; diff --git a/js/presetTextures.js b/js/presetTextures.js index 1b80eff..7ad1295 100644 --- a/js/presetTextures.js +++ b/js/presetTextures.js @@ -35,24 +35,24 @@ const IMAGE_PRESETS = [ { name: 'Carbon Fiber', url: 'textures/carbonFiber.jpg', defaultScale: 0.5 }, { name: 'Crystal', url: 'textures/crystal.jpg', defaultScale: 0.5 }, { name: 'Dots', url: 'textures/dots.jpg', defaultScale: 0.1 }, + { name: 'Grid', url: 'textures/grid.png', defaultScale: 1.0 }, { name: 'Grip Surface', url: 'textures/gripSurface.jpg', defaultScale: 0.5 }, - { name: 'Hexagon', url: 'textures/hexagon.jpg', defaultScale: 0.5 }, + { name: 'Hexagon', url: 'textures/hexagon.jpg', defaultScale: 0.5 }, { name: 'Hexagons', url: 'textures/hexagons.jpg', defaultScale: 1.0 }, - { name: 'Isogrid', url: 'textures/isogrid.png', defaultScale: 0.5 }, + { name: 'Isogrid', url: 'textures/isogrid.png', defaultScale: 0.5 }, { name: 'Knitting', url: 'textures/knitting.jpg', defaultScale: 0.25 }, { name: 'Knurling', url: 'textures/knurling.jpg', defaultScale: 0.15 }, - { name: 'Leather', url: 'textures/leather.jpg', defaultScale: 0.5 }, { name: 'Leather 2', url: 'textures/leather2.jpg', defaultScale: 0.5 }, { name: 'Noise', url: 'textures/noise.jpg', defaultScale: 0.3 }, - { name: 'Stripes', url: 'textures/stripes.png', defaultScale: 0.5 }, - { name: 'Stripes 02', url: 'textures/stripes_02.png', defaultScale: 1.0 }, + { name: 'Stripes 1', url: 'textures/stripes.png', defaultScale: 0.5 }, + { name: 'Stripes 2', url: 'textures/stripes_02.png', defaultScale: 1.0 }, { name: 'Voronoi', url: 'textures/voronoi.jpg', defaultScale: 0.5 }, - { name: 'Weave', url: 'textures/weave.jpg', defaultScale: 0.5 }, - { name: 'Weave 02', url: 'textures/weave_02.jpg', defaultScale: 0.5 }, - { name: 'Weave 03', url: 'textures/weave_03.jpg', defaultScale: 0.5 }, - { name: 'Wood', url: 'textures/wood.jpg', defaultScale: 0.5 }, - { name: 'Wood Grain 02',url: 'textures/woodgrain_02.jpg', defaultScale: 1.0 }, - { name: 'Wood Grain 03',url: 'textures/woodgrain_03.jpg', defaultScale: 1.0 }, + { name: 'Weave 1', url: 'textures/weave.jpg', defaultScale: 0.5 }, + { name: 'Weave 2', url: 'textures/weave_02.jpg', defaultScale: 0.5 }, + { name: 'Weave 3', url: 'textures/weave_03.jpg', defaultScale: 0.5 }, + { name: 'Wood 1', url: 'textures/wood.jpg', defaultScale: 0.5 }, + { name: 'Wood 2', url: 'textures/woodgrain_02.jpg', defaultScale: 1.0 }, + { name: 'Wood 3', url: 'textures/woodgrain_03.jpg', defaultScale: 1.0 }, ]; function loadImagePreset(preset) { diff --git a/textures/grid.png b/textures/grid.png new file mode 100644 index 0000000000000000000000000000000000000000..d4898e7e72fcc94215d1d441c987d0e4458b044b GIT binary patch literal 3251 zcmeHIU1%It6rNP8O=2M6ONmIQ6GTO4=gx0-hOBMejY-f=V;XF1wC$a_cam{;ew>+P zH&NTthoBGj2cZ}V6a=9!HWEmn2&Vd=g*JVXlvE3X&^(An1re-L?`(FbTa&aU&6BvW zyEAvbIo~lYM)HT&n6xGnxndzmdnyKi!Z3|g3YUf`df3;rcivdN|KNx*$ zs1GN%Q`G%6R$qVEpM6dv+&|Qq1yaa|~^v6thpx zf~=RuLssXQkN1vs_Zee3Lq*K4r|8zAMgrJ4gmlr)JAqbAF{QW~xsH}OhAu(ET#9Ln z2>O!FWCSOWk+3v!|(fdqghMR}4IfFN_cz=0$y08Iv(z|-XqLl}KzYQ33f z%6#N5#SDd^r*T}NP)HQSgzFD-ysD}k5I8|#34#qqoe&mTCupuj$l$>6EibfOhmImb z-5m~749Rw-4BLytIzd?~A{tkO9>*s@R8|Ryj2Pz)`}q<%GB}*aHg>{*VEGv94Y{Ek z47uxsjvwDdK;+G4V~#a*vF(^52-`QXTH41j zbp1Zp%~zV$Q%RW?S`rfd*gnfaZXtNQ>IIyEAx<%*YYGcQR^mth>t%gc0AW+g^SrgQopu{!^hVZsvWN4=A+mIA&*>DhZo-@eMv5Z>U&AUD!CfSLl zEV5ax%Lzj07`Q8wVo1#i%R-u=h(J#oJZpj$z$Og|v#NxWtS(4NpqS*og|EDyagE`q zv(fu2Mn|qeV%%Ug1tjU%6xpO{V%Ah8lT~#^WO*d&k_2U$m*Qx9e2eTgn6Hi+WravY zMMQuncvhBm5(ks4V2jMNlCCIvQb<1~v^j({Vo|m@_7}d&+IHt#3KeUS)@n`tLsTJ zu#jqE5kgTgg^I+hBqCD{BsGz*n0_M?mE|{vpfiZckmi_;7jtL+U&*q%m^F9s<`u(5 zb9u$d+`mm;dR8^ViD61hVBWxnpp z?#8Wc$Cr;R$q&~bX>UfS>K+~aw5{W{mrF+@`8hi`uP*He;vYZc2A)mND&zUZKhG_k zJpRbh>u)!0$u!h_G)_IAK6+~B?r$3DSGG0E_rAHY<(Tk&b?K#g^XlQ(?VNRP;j4Gq zfgOv#U2D3l;HxsMuu*%iP6q4;6W4y7`Q+oB0}rKV^KYQ@%ehS`sERXYYn=!#&Kxp- z{`%7FiSfMsz~c1p@3C`Jne%M;J5pmre}C`exYObug~w>V27#- zs%~?I?(BcKadQ_|Z-NuoH_7=`x>mIOpYJXvNkz|!m{d_%n#_H1;k$RHY_?(X^7d)` mWAnh_gZFIW^dg)GzUScl^p~g4{Cyx|?rQJOoPPSyv3~%xQt{>h literal 0 HcmV?d00001 diff --git a/textures/stripes.jpg b/textures/stripes.jpg deleted file mode 100644 index 7160d073244da2e0b7768142348517b1141c92f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48429 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