From 939ce8fcc96fab8bf7fe38239d863002ad1bd710 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 19 Sep 2026 17:58:25 +0200 Subject: [PATCH] fix(i18n): add 'home' body class to English homepage so section__title margin applies --- IDEA.md | 1 + _fab_ds.png | Bin 0 -> 23975 bytes _fab_lunacy.png | Bin 0 -> 11271 bytes _menu2.mjs | 18 +++++ allsh.mjs | 23 ++++++ anim.mjs | 20 +++++ anim2.mjs | 18 +++++ anim3.mjs | 22 +++++ animlong.mjs | 22 +++++ basicshader.mjs | 26 ++++++ bright.mjs | 18 +++++ checkshadows.mjs | 21 +++++ chk2.mjs | 18 +++++ chk3.mjs | 13 +++ clean.mjs | 22 +++++ cmp.mjs | 23 ++++++ cmp2.mjs | 16 ++++ cmp3.mjs | 11 +++ cmpf.mjs | 19 +++++ console.mjs | 22 +++++ correct.mjs | 32 ++++++++ ctor.mjs | 25 ++++++ demochk.mjs | 24 ++++++ democrop.mjs | 21 +++++ demodata.mjs | 34 ++++++++ demofull.mjs | 20 +++++ demolast.mjs | 21 +++++ demook.mjs | 26 ++++++ demopic.mjs | 11 +++ demovis.mjs | 28 +++++++ diag.mjs | 30 +++++++ doccheck.mjs | 21 +++++ final.mjs | 16 ++++ final2.mjs | 16 ++++ fixcheck.mjs | 12 +++ frametest.mjs | 20 +++++ glerr.mjs | 19 +++++ glerr2.mjs | 19 +++++ grid.mjs | 24 ++++++ locs.mjs | 17 ++++ manual.mjs | 21 +++++ matrix.mjs | 23 ++++++ meshcheck.mjs | 18 +++++ mountcheck.mjs | 12 +++ pixeltime.mjs | 20 +++++ probe.mjs | 26 ++++++ probe_tmp.js | 52 ++++++++++++ public/en/index.html | 2 +- pulse.mjs | 20 +++++ pure.mjs | 14 ++++ raf.mjs | 32 ++++++++ readback.mjs | 27 +++++++ readu.mjs | 22 +++++ real.mjs | 16 ++++ rendercheck.mjs | 18 +++++ res.mjs | 19 +++++ res2.mjs | 14 ++++ selfraf.mjs | 14 ++++ setframe.mjs | 15 ++++ setu.mjs | 17 ++++ setu2.mjs | 21 +++++ shadow.mjs | 24 ++++++ shchk.mjs | 24 ++++++ shclick.mjs | 22 +++++ shelem.mjs | 24 ++++++ shelem2.mjs | 24 ++++++ shelem3.mjs | 24 ++++++ shfin.mjs | 16 ++++ shfinal.mjs | 23 ++++++ shfix.mjs | 26 ++++++ shlog.mjs | 21 +++++ shot.mjs | 12 +++ shot_tmp.js | 14 ++++ shotcmp.mjs | 16 ++++ shreal.mjs | 16 ++++ shver.mjs | 32 ++++++++ shvis.mjs | 17 ++++ size.mjs | 19 +++++ src/pages/index.en.html | 2 +- texts/cases/card-columns-obsidian.md | 116 +++++++++++++++++++++++++++ two.mjs | 14 ++++ twoframes.mjs | 21 +++++ ucol.mjs | 20 +++++ uloc.mjs | 22 +++++ utime.mjs | 19 +++++ verify.mjs | 23 ++++++ vp.mjs | 21 +++++ webgl.mjs | 28 +++++++ 88 files changed, 1830 insertions(+), 2 deletions(-) create mode 100644 IDEA.md create mode 100644 _fab_ds.png create mode 100644 _fab_lunacy.png create mode 100644 _menu2.mjs create mode 100644 allsh.mjs create mode 100644 anim.mjs create mode 100644 anim2.mjs create mode 100644 anim3.mjs create mode 100644 animlong.mjs create mode 100644 basicshader.mjs create mode 100644 bright.mjs create mode 100644 checkshadows.mjs create mode 100644 chk2.mjs create mode 100644 chk3.mjs create mode 100644 clean.mjs create mode 100644 cmp.mjs create mode 100644 cmp2.mjs create mode 100644 cmp3.mjs create mode 100644 cmpf.mjs create mode 100644 console.mjs create mode 100644 correct.mjs create mode 100644 ctor.mjs create mode 100644 demochk.mjs create mode 100644 democrop.mjs create mode 100644 demodata.mjs create mode 100644 demofull.mjs create mode 100644 demolast.mjs create mode 100644 demook.mjs create mode 100644 demopic.mjs create mode 100644 demovis.mjs create mode 100644 diag.mjs create mode 100644 doccheck.mjs create mode 100644 final.mjs create mode 100644 final2.mjs create mode 100644 fixcheck.mjs create mode 100644 frametest.mjs create mode 100644 glerr.mjs create mode 100644 glerr2.mjs create mode 100644 grid.mjs create mode 100644 locs.mjs create mode 100644 manual.mjs create mode 100644 matrix.mjs create mode 100644 meshcheck.mjs create mode 100644 mountcheck.mjs create mode 100644 pixeltime.mjs create mode 100644 probe.mjs create mode 100644 probe_tmp.js create mode 100644 pulse.mjs create mode 100644 pure.mjs create mode 100644 raf.mjs create mode 100644 readback.mjs create mode 100644 readu.mjs create mode 100644 real.mjs create mode 100644 rendercheck.mjs create mode 100644 res.mjs create mode 100644 res2.mjs create mode 100644 selfraf.mjs create mode 100644 setframe.mjs create mode 100644 setu.mjs create mode 100644 setu2.mjs create mode 100644 shadow.mjs create mode 100644 shchk.mjs create mode 100644 shclick.mjs create mode 100644 shelem.mjs create mode 100644 shelem2.mjs create mode 100644 shelem3.mjs create mode 100644 shfin.mjs create mode 100644 shfinal.mjs create mode 100644 shfix.mjs create mode 100644 shlog.mjs create mode 100644 shot.mjs create mode 100644 shot_tmp.js create mode 100644 shotcmp.mjs create mode 100644 shreal.mjs create mode 100644 shver.mjs create mode 100644 shvis.mjs create mode 100644 size.mjs create mode 100644 texts/cases/card-columns-obsidian.md create mode 100644 two.mjs create mode 100644 twoframes.mjs create mode 100644 ucol.mjs create mode 100644 uloc.mjs create mode 100644 utime.mjs create mode 100644 verify.mjs create mode 100644 vp.mjs create mode 100644 webgl.mjs diff --git a/IDEA.md b/IDEA.md new file mode 100644 index 0000000..7f114c8 --- /dev/null +++ b/IDEA.md @@ -0,0 +1 @@ +Мой сайт портфолио diff --git a/_fab_ds.png b/_fab_ds.png new file mode 100644 index 0000000000000000000000000000000000000000..98a39636de3eb67c2011a556ec4e5e99b68bc1dd GIT binary patch literal 23975 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 R|M2YJ)s(fBO72^}{4Ws64j2Fc literal 0 HcmV?d00001 diff --git a/_fab_lunacy.png b/_fab_lunacy.png new file mode 100644 index 0000000000000000000000000000000000000000..65b3b8015c0ffd21e2cd881b371eab4fb07f0f35 GIT binary patch literal 11271 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 x^hdjo{nmc~00960OW+In00006Nkl window.scrollTo(0, 400)); +await new Promise(r=>setTimeout(r,400)); +await p.evaluate(() => document.querySelector("[data-nav-more-btn]")?.click()); +await new Promise(r=>setTimeout(r,300)); +const m = await p.evaluate(() => { + const menu = document.querySelector("#nav-more-menu-floating"); + if (!menu || menu.hidden) return { hidden: true }; + const mr = menu.getBoundingClientRect(); + const vw = document.documentElement.clientWidth; + return { rightFromEdge: Math.round(vw - mr.right), edgePad: getComputedStyle(document.documentElement).getPropertyValue("--edge-pad").trim() }; +}); +console.log("MENU@768:", JSON.stringify(m)); +await b.close(); diff --git a/allsh.mjs b/allsh.mjs new file mode 100644 index 0000000..dd12e25 --- /dev/null +++ b/allsh.mjs @@ -0,0 +1,23 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());}); +await p.setViewport({width:1100,height:1500}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +await p.evaluate(()=>document.querySelector('#sec-all-shaders')?.scrollIntoView()); +await new Promise(r=>setTimeout(r,1500)); +const r = await p.evaluate(()=>{ + const cells=[...document.querySelectorAll('[data-shader-cell]')]; + const cellInfo=cells.map(c=>{const m=c.querySelector('.card-shader')?.paperShaderMount; return {key:c.dataset.shaderCell, mounted:!!m, speed:m?.currentSpeed, frame:m?Math.round(m.getCurrentFrame()):null};}); + const demo=document.querySelector('[data-shader-demo]'); + const demoMounted=!!demo.querySelector('.card-shader')?.paperShaderMount; + // переключим на spiral и проверим label + const btn=document.querySelector('[data-shader-switch="spiral"]'); + let labelAfter=null; + if(btn){ btn.click(); labelAfter=document.querySelector('[data-shader-label]')?.textContent; } + return { cellCount: cells.length, cellInfo, demoMounted, labelAfter }; +}); +console.log(JSON.stringify(r,null,1)); +console.log('ERRS', errs.slice(0,8)); +await b.close(); diff --git a/anim.mjs b/anim.mjs new file mode 100644 index 0000000..d26f97a --- /dev/null +++ b/anim.mjs @@ -0,0 +1,20 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +await p.evaluate(()=>document.querySelector('[data-shader-demo]').scrollIntoView({block:'center'})); +await new Promise(r=>setTimeout(r,600)); +const snap = ()=>p.evaluate(()=>{ + const cv=document.querySelector('[data-shader-demo] canvas'); + const gl=cv.getContext('webgl2',{preserveDrawingBuffer:true})||cv.getContext('webgl',{preserveDrawingBuffer:true}); + const a=new Uint8Array(4*9); let k=0; + for(const [x,y] of [[20,20],[100,80],[200,150],[50,180],[300,40]]){ gl.readPixels(x,y,1,1,gl.RGBA,gl.UNSIGNED_BYTE,a.subarray(k,k+4)); k+=4; } + return Array.from(a).join(','); +}); +const s1=await snap(); await new Promise(r=>setTimeout(r,700)); const s2=await snap(); +console.log('frame1:', s1); +console.log('frame2:', s2); +console.log('MOVING:', s1!==s2); +await b.close(); diff --git a/anim2.mjs b/anim2.mjs new file mode 100644 index 0000000..07b83ec --- /dev/null +++ b/anim2.mjs @@ -0,0 +1,18 @@ +import puppeteer from 'puppeteer'; +import sharp from 'sharp'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +await p.evaluate(()=>document.querySelector('[data-shader-demo]').scrollIntoView({block:'center'})); +await new Promise(r=>setTimeout(r,600)); +async function shot(){ const el=await p.$('[data-shader-demo]'); await el.screenshot({path:'/tmp/a.png'}); + const {data,info}=await sharp('/tmp/a.png').raw().toBuffer({resolveWithObject:true}); + return {w:info.width,h:info.height,data}; } +const s1=await shot(); await new Promise(r=>setTimeout(r,800)); const s2=await shot(); +// сравним центральные пиксели +function center(d,info){ const i=((info.height>>1)*info.width+(info.width>>1))*3; return [d[i],d[i+1],d[i+2]]; } +const c1=center(s1.data,s1), c2=center(s2.data,s2); +console.log('center px t1:', c1, ' t2:', c2, ' MOVED:', JSON.stringify(c1)!==JSON.stringify(c2)); +await b.close(); diff --git a/anim3.mjs b/anim3.mjs new file mode 100644 index 0000000..d7bbb2b --- /dev/null +++ b/anim3.mjs @@ -0,0 +1,22 @@ +import puppeteer from 'puppeteer'; +import sharp from 'sharp'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const top = await p.evaluate(()=>{ const r=document.querySelector('[data-shader-demo]').getBoundingClientRect(); return r.top+window.scrollY; }); +await p.evaluate((t)=>window.scrollTo(0, t-150), top); +await new Promise(r=>setTimeout(r,700)); +async function region(){ await p.screenshot({path:'/tmp/r.png'}); + const rect = await p.evaluate(()=>{ const r=document.querySelector('[data-shader-demo]').getBoundingClientRect(); return {x:r.x,y:r.y,w:r.width,h:r.height}; }); + const {data,info}=await sharp('/tmp/r.png').raw().toBuffer({resolveWithObject:true}); + // хеш центральной полосы + const samples=[]; const y=Math.floor(rect.y+rect.h*0.4); + for(let x=Math.floor(rect.x+20); xsetTimeout(r,800)); const c=await region(); +console.log('moved:', a!==c); +console.log('t1 head:', a.slice(0,60)); console.log('t2 head:', c.slice(0,60)); +await b.close(); diff --git a/animlong.mjs b/animlong.mjs new file mode 100644 index 0000000..d8e68d8 --- /dev/null +++ b/animlong.mjs @@ -0,0 +1,22 @@ +import puppeteer from 'puppeteer'; +import sharp from 'sharp'; +import crypto from 'crypto'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const top = await p.evaluate(()=>{ const r=document.querySelector('[data-shader-demo]').getBoundingClientRect(); return r.top+window.scrollY; }); +await p.evaluate((t)=>window.scrollTo(0, t-150), top); +await new Promise(r=>setTimeout(r,700)); +async function hash(){ await p.screenshot({path:'/tmp/rl.png'}); + const rect = await p.evaluate(()=>{ const r=document.querySelector('[data-shader-demo]').getBoundingClientRect(); return {x:r.x,y:r.y,w:r.width,h:r.height}; }); + const {data,info}=await sharp('/tmp/rl.png').raw().toBuffer({resolveWithObject:true}); + const x0=Math.floor(rect.x),y0=Math.floor(rect.y),x1=Math.floor(rect.x+rect.w),y1=Math.floor(rect.y+rect.h); + const buf=Buffer.alloc((x1-x0)*(y1-y0)*3); + let k=0; for(let y=y0;ysetTimeout(r,3000)); const c=await hash(); +console.log('hash1', a); console.log('hash2', c); console.log('ANIMATED:', a!==c); +await b.close(); diff --git a/basicshader.mjs b/basicshader.mjs new file mode 100644 index 0000000..8c7dc5c --- /dev/null +++ b/basicshader.mjs @@ -0,0 +1,26 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:800,height:600}); +await p.goto('about:blank',{waitUntil:'load'}); +await new Promise(r=>setTimeout(r,300)); +const r = await p.evaluate(()=>{ + const cv=document.createElement('canvas'); cv.width=64; cv.height=64; document.body.appendChild(cv); + const gl=cv.getContext('webgl2'); + if(!gl) return {webgl2:false}; + const vs=`#version 300 es\nin vec4 a_position; void main(){ gl_Position=a_position; }`; + const fs=`#version 300 es\nprecision mediump float; out vec4 o; void main(){ o=vec4(1.0,0.3,0.0,1.0); }`; + const mk=(s)=>{const sh=gl.createShader(s.includes('main(){ gl_Position')?gl.VERTEX_SHADER:gl.FRAGMENT_SHADER);gl.shaderSource(sh,s);gl.compileShader(sh);return {sh,ok:gl.getShaderParameter(sh,gl.COMPILE_STATUS),log:gl.getShaderInfoLog(sh)};}; + const v=mk(vs), f=mk(fs); + const prog=gl.createProgram(); gl.attachShader(prog,v.sh); gl.attachShader(prog,f.sh); gl.linkProgram(prog); + const linked=gl.getProgramParameter(prog,gl.LINK_STATUS); + gl.useProgram(prog); + const buf=gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER,buf); gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),gl.STATIC_DRAW); + const loc=gl.getAttribLocation(prog,'a_position'); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc,2,gl.FLOAT,false,0,0); + gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT); + gl.drawArrays(gl.TRIANGLES,0,6); + const px=new Uint8Array(4); gl.readPixels(32,32,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px); + return { webgl2:true, vsOk:v.ok, fsOk:f.ok, fsLog:f.log, linked, center:Array.from(px) }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/bright.mjs b/bright.mjs new file mode 100644 index 0000000..b22f7f3 --- /dev/null +++ b/bright.mjs @@ -0,0 +1,18 @@ +import puppeteer from 'puppeteer'; +import sharp from 'sharp'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const top = await p.evaluate(()=>{ const r=document.querySelector('[data-shader-demo]').getBoundingClientRect(); return r.top+window.scrollY; }); +await p.evaluate((t)=>window.scrollTo(0, t-150), top); +await new Promise(r=>setTimeout(r,800)); +await p.screenshot({path:'/tmp/sec.png'}); +const rect = await p.evaluate(()=>{ const r=document.querySelector('[data-shader-demo]').getBoundingClientRect(); return {x:r.x,y:r.y,w:r.width,h:r.height}; }); +const {data,info}=await sharp('/tmp/sec.png').raw().toBuffer({resolveWithObject:true}); +const x0=Math.floor(rect.x), y0=Math.floor(rect.y), x1=Math.floor(rect.x+rect.w), y1=Math.floor(rect.y+rect.h); +let maxR=0,maxG=0,maxB=0, brightPx=0, n=0; +for(let y=Math.max(0,y0);y250) brightPx++; n++; } +console.log('max channel values in demo card:', maxR, maxG, maxB, 'bright pixels:', brightPx, '/', n); +await b.close(); diff --git a/checkshadows.mjs b/checkshadows.mjs new file mode 100644 index 0000000..44aaf7a --- /dev/null +++ b/checkshadows.mjs @@ -0,0 +1,21 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());}); +await p.setViewport({width:1100,height:1500}); +await p.goto('http://10.200.201.3:4175/',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,800)); +const r = await p.evaluate(()=>{ + const pick = (sel)=>{ const el=document.querySelector(sel); if(!el) return null; + const cs=getComputedStyle(el); + return { boxShadow: cs.boxShadow.slice(0,60), shadowLen: cs.boxShadow.length, transform: cs.transform, bg: cs.backgroundImage.slice(0,40) }; }; + return { + mp: pick('.mp-card'), + portfolio: pick('.portfolio-card'), + skill: pick('.skill-task-card'), + theme: document.documentElement.getAttribute('data-theme'), + }; +}); +console.log(JSON.stringify(r,null,1)); +console.log('ERRS', errs.slice(0,6)); +await b.close(); diff --git a/chk2.mjs b/chk2.mjs new file mode 100644 index 0000000..a78b186 --- /dev/null +++ b/chk2.mjs @@ -0,0 +1,18 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +const errs=[]; p.on('console',m=>{ if(m.type()==='error') errs.push(m.text()); }); +p.on('pageerror',e=>errs.push('PAGEERR '+e.message)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +const cards = await p.$$eval('#sec-contact .contact-card', els => els.map(e=>{ + const cs = getComputedStyle(e); + const before = getComputedStyle(e,'::before'); + const img = e.querySelector('img'); + return { name:e.querySelector('.contact-card__name')?.textContent, + boxShadow: cs.boxShadow.slice(0,40), + beforeBg: before.backgroundImage.slice(0,60), + borderW: cs.borderTopWidth, + imgLoaded: img ? (img.complete && img.naturalWidth>0) : 'no-img' }; +})); +console.log(JSON.stringify({count:cards.length, cards, errs}, null, 1)); +await b.close(); diff --git a/chk3.mjs b/chk3.mjs new file mode 100644 index 0000000..3b841be --- /dev/null +++ b/chk3.mjs @@ -0,0 +1,13 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +const errs=[]; p.on('console',m=>{ if(m.type()==='error') errs.push(m.text()); }); +p.on('pageerror',e=>errs.push('PAGEERR '+e.message)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +// dark +const dark = await p.$$eval('#sec-contact .contact-card', els => els.map(e=>({n:e.querySelector('.contact-card__name')?.textContent, bs:e.style.boxShadow?'inline':'', shadow:getComputedStyle(e).boxShadow.length}))); +// light theme +await p.evaluate(()=>document.documentElement.setAttribute('data-theme','light')); +const light = await p.$$eval('#sec-contact .contact-card', els => els.map(e=>({n:e.querySelector('.contact-card__name')?.textContent, shadow:getComputedStyle(e).boxShadow.slice(0,30)}))); +console.log(JSON.stringify({darkCount:dark.length, light, errs}, null, 1)); +await b.close(); diff --git a/clean.mjs b/clean.mjs new file mode 100644 index 0000000..3facf85 --- /dev/null +++ b/clean.mjs @@ -0,0 +1,22 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());}); +await p.setViewport({width:1100,height:1400}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +const r = await p.evaluate(()=>{ + const shaderCards=[...document.querySelectorAll('.card-shader[data-shader]')]; + const mounted=shaderCards.map(w=>!!w.paperShaderMount); + return { + hasSecShader: !!document.querySelector('#sec-shader'), + hasSecShaderGrid: !!document.querySelector('#sec-shader-grid'), + hasSecContact: !!document.querySelector('#sec-contact'), + totalShaderWraps: shaderCards.length, + allMounted: mounted.every(Boolean), + mountedCount: mounted.filter(Boolean).length, + }; +}); +console.log(JSON.stringify(r,null,1)); +console.log('ERRS', errs.slice(0,5)); +await b.close(); diff --git a/cmp.mjs b/cmp.mjs new file mode 100644 index 0000000..39b8e6c --- /dev/null +++ b/cmp.mjs @@ -0,0 +1,23 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +await p.setViewport({width:1280,height:1000}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); + +const probe = async (sel) => p.evaluate((s)=>{ + const e = document.querySelector(s); + const cs = getComputedStyle(e); + const bs = cs.boxShadow.split('),').map(x=>x.trim()); + return { + bg: cs.backgroundColor, + boxShadow: bs, + }; +}, sel); + +const neu = await probe('#sec-final .neu-card'); +const cc = await probe('#sec-contact .contact-card'); +console.log('=== NEU-CARD (Финал) ==='); +console.log(JSON.stringify(neu,null,1)); +console.log('=== CONTACT-CARD ==='); +console.log(JSON.stringify(cc,null,1)); +await b.close(); diff --git a/cmp2.mjs b/cmp2.mjs new file mode 100644 index 0000000..a7e8f12 --- /dev/null +++ b/cmp2.mjs @@ -0,0 +1,16 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +const probe = async (sel) => p.evaluate((s)=>{ + const e = document.querySelector(s); const cs=getComputedStyle(e); + return { bg: cs.backgroundColor, bgImg: cs.backgroundImage.slice(0,55), shadow: cs.boxShadow.split('),').map(x=>x.trim().replace(/rgba\([^)]*\)/,'C')) }; +}, sel); +console.log('DARK'); +console.log(' neu:', JSON.stringify(await probe('#sec-final .neu-card'))); +console.log(' cc :', JSON.stringify(await probe('#sec-contact .contact-card'))); +await p.evaluate(()=>document.documentElement.setAttribute('data-theme','light')); +console.log('LIGHT'); +console.log(' neu:', JSON.stringify(await probe('#sec-final .neu-card'))); +console.log(' cc :', JSON.stringify(await probe('#sec-contact .contact-card'))); +await b.close(); diff --git a/cmp3.mjs b/cmp3.mjs new file mode 100644 index 0000000..cfbc9df --- /dev/null +++ b/cmp3.mjs @@ -0,0 +1,11 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +const probe = async (sel) => p.evaluate((s)=>{ + const e = document.querySelector(s); const cs=getComputedStyle(e); + return { bgImg: cs.backgroundImage.slice(0,60) }; +}, sel); +console.log('DARK neu bgImg:', (await probe('#sec-final .neu-card')).bgImg); +console.log('DARK cc bgImg:', (await probe('#sec-contact .contact-card')).bgImg); +await b.close(); diff --git a/cmpf.mjs b/cmpf.mjs new file mode 100644 index 0000000..7aa2351 --- /dev/null +++ b/cmpf.mjs @@ -0,0 +1,19 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +const errs=[]; p.on('console',m=>{if(m.type()==='error')errs.push(m.text())}); p.on('pageerror',e=>errs.push('PE '+e.message)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +const probe = async (sel) => p.evaluate((s)=>{ + const e=document.querySelector(s); const cs=getComputedStyle(e); + return { bg: cs.backgroundColor, bgImg: cs.backgroundImage.slice(0,45), shadowLen: cs.boxShadow.length, + dir: cs.flexDirection, pad: cs.paddingTop }; +}, sel); +const neu = await probe('#sec-final .neu-card'); +const cc = await probe('#sec-contact .contact-card'); +const neuShadow = await p.evaluate(()=>getComputedStyle(document.querySelector('#sec-final .neu-card')).boxShadow); +const ccShadow = await p.evaluate(()=>getComputedStyle(document.querySelector('#sec-contact .contact-card')).boxShadow); +console.log('neu:', JSON.stringify(neu)); +console.log('cc :', JSON.stringify(cc)); +console.log('shadows equal:', neuShadow===ccShadow); +console.log('errs:', errs); +await b.close(); diff --git a/console.mjs b/console.mjs new file mode 100644 index 0000000..e544346 --- /dev/null +++ b/console.mjs @@ -0,0 +1,22 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const logs=[]; +p.on('console', m=>logs.push('['+m.type()+'] '+m.text())); +p.on('pageerror', e=>logs.push('[pageerror] '+e.message)); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,800)); +const r = await p.evaluate(async ()=>{ + const mod = await import('/vendor/paper-shaders/index.js'); + const { ShaderMount, meshGradientFragmentShader, getShaderColorFromString } = mod; + const host = document.createElement('div'); host.style.cssText='width:300px;height:200px;position:relative'; + document.body.appendChild(host); + const colors = ['#7c5cff','#00c2c7','#ff5c8a','#ffb347'].map(getShaderColorFromString); + const uniforms = { u_colors: colors, u_colorsCount: colors.length, u_distortion:1.2, u_swirl:0.8, u_grainMixer:0.08, u_grainOverlay:0.04 }; + const m = new ShaderMount(host, meshGradientFragmentShader, uniforms, { preserveDrawingBuffer:true, antialias:true }, 1.0); + return { keys: Object.keys(m.uniformLocations), uColorsLoc: m.uniformLocations.u_colors, uColorsCountLoc: m.uniformLocations.u_colorsCount }; +}); +console.log('RESULT', JSON.stringify(r)); +console.log('LOGS:\n'+logs.join('\n')); +await b.close(); diff --git a/correct.mjs b/correct.mjs new file mode 100644 index 0000000..83adb3f --- /dev/null +++ b/correct.mjs @@ -0,0 +1,32 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,800)); +const r = await p.evaluate(async ()=>{ + const mod = await import('/vendor/paper-shaders/index.js'); + const { ShaderMount, meshGradientFragmentShader, getShaderColorFromString } = mod; + const host = document.createElement('div'); host.style.cssText='width:300px;height:200px;position:relative'; + document.body.appendChild(host); + const colors = ['#7c5cff','#00c2c7','#ff5c8a','#ffb347'].map(getShaderColorFromString); + const m = new ShaderMount(host, meshGradientFragmentShader, { u_colors: colors, u_colorsCount: colors.length, u_distortion:1.2, u_swirl:0.8, u_grainMixer:0.08, u_grainOverlay:0.04 }, { preserveDrawingBuffer:true, antialias:true }, 1.0); + const gl=m.gl, pr=m.program; + // правильное чтение: getUniform ВОЗВРАЩАЕТ значение + const gf=(n)=>gl.getUniform(pr, gl.getUniformLocation(pr,n)); + const colorsWritten = gf('u_colors[0]'); // Float32Array(4) + const countWritten = gf('u_colorsCount'); // number + const swirlWritten = gf('u_swirl'); + // readPixels скан + m.render(performance.now()); m.render(performance.now()); + const W=m.canvasElement.width,H=m.canvasElement.height; const buf=new Uint8Array(W*H*4); gl.readPixels(0,0,W,H,gl.RGBA,gl.UNSIGNED_BYTE,buf); + let bright=0,maxc=0; for(let i=0;i120)bright++; maxc=Math.max(maxc,mx);} + return { + u_colors0: colorsWritten ? Array.from(colorsWritten).map(x=>+x.toFixed(2)) : null, + u_colorsCount: countWritten, + u_swirl: swirlWritten, + brightPixels: bright, maxChannel: maxc, + }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/ctor.mjs b/ctor.mjs new file mode 100644 index 0000000..05abcc2 --- /dev/null +++ b/ctor.mjs @@ -0,0 +1,25 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,800)); +const r = await p.evaluate(async ()=>{ + const mod = await import('/vendor/paper-shaders/index.js'); + const { ShaderMount, meshGradientFragmentShader, getShaderColorFromString } = mod; + const host = document.createElement('div'); host.style.cssText='width:300px;height:200px;position:relative'; + document.body.appendChild(host); + const colors = ['#7c5cff','#00c2c7','#ff5c8a','#ffb347'].map(getShaderColorFromString); + const uniforms = { u_colors: colors, u_colorsCount: colors.length, u_distortion:1.2, u_swirl:0.8, u_grainMixer:0.08, u_grainOverlay:0.04 }; + console.log('uniforms passed:', JSON.stringify(uniforms.u_colors), 'count', uniforms.u_colorsCount); + const m = new ShaderMount(host, meshGradientFragmentShader, uniforms, { preserveDrawingBuffer:true, antialias:true }, 1.0); + m.render(performance.now()); m.render(performance.now()); + const gl=m.gl; + const cnt=new Float32Array(1); gl.getUniform(m.program, gl.getUniformLocation(m.program,'u_colorsCount'), cnt); + const c0=new Float32Array(4); gl.getUniform(m.program, gl.getUniformLocation(m.program,'u_colors[0]'), c0); + const W=m.canvasElement.width,H=m.canvasElement.height; const buf=new Uint8Array(W*H*4); gl.readPixels(0,0,W,H,gl.RGBA,gl.UNSIGNED_BYTE,buf); + let bright=0,maxc=0; for(let i=0;i120)bright++; maxc=Math.max(maxc,mx);} + return { count:cnt[0], c0:Array.from(c0).map(x=>+x.toFixed(2)), bright, maxc }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/demochk.mjs b/demochk.mjs new file mode 100644 index 0000000..51c17a5 --- /dev/null +++ b/demochk.mjs @@ -0,0 +1,24 @@ +import puppeteer from 'puppeteer'; +import sharp from 'sharp'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1280,height:1200}); +const errs=[]; p.on('console',m=>{if(m.type()==='error')errs.push(m.text())}); p.on('pageerror',e=>errs.push('PE '+e.message)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const demo = await p.$('[data-shader-demo]'); +await demo.evaluate(el=>el.scrollIntoView({block:'center'})); +await new Promise(r=>setTimeout(r,400)); +async function probe(key){ + if(key){ await p.click(`[data-shader-switch="${key}"]`); await new Promise(r=>setTimeout(r,700)); } + const el = await p.$('[data-shader-demo]'); + const box = await el.boundingBox(); + await el.screenshot({path:'/tmp/demo.png'}); + const {data,info}=await sharp('/tmp/demo.png').raw().toBuffer({resolveWithObject:true}); + let n=info.width*info.height, rs=0,gs=0,bs=0; const cols=new Set(); + for(let i=0;i>5},${data[i+1]>>5},${data[i+2]>>5}`); } + return { key: key||'mesh', avg:[Math.round(rs/n),Math.round(gs/n),Math.round(bs/n)], distinct: cols.size }; +} +const r1=await probe(); const r2=await probe('grain'); const r3=await probe('voronoi'); const r4=await probe('staticMesh'); +console.log(JSON.stringify({r1,r2,r3,r4,errs},null,1)); +await b.close(); diff --git a/democrop.mjs b/democrop.mjs new file mode 100644 index 0000000..2485ec6 --- /dev/null +++ b/democrop.mjs @@ -0,0 +1,21 @@ +import puppeteer from 'puppeteer'; +import sharp from 'sharp'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +await p.evaluate(()=>document.querySelector('#sec-shader').scrollIntoView()); +await new Promise(r=>setTimeout(r,900)); +// координаты демо-карточки +const rect = await p.evaluate(()=>{ const r=document.querySelector('[data-shader-demo]').getBoundingClientRect(); return {x:r.x,y:r.y,w:r.width,h:r.height}; }); +await p.screenshot({path:'/tmp/full.png'}); +const img = sharp('/tmp/full.png'); +const {data,info}=await img.raw().toBuffer({resolveWithObject:true}); +// вырежем центр демо-карточки (по координатам вьюпорта) +const x0=Math.floor(rect.x+rect.w*0.2), y0=Math.floor(rect.y+rect.h*0.2); +const x1=Math.floor(rect.x+rect.w*0.8), y1=Math.floor(rect.y+rect.h*0.8); +let rs=0,gs=0,bs=0,n=0; const cols=new Set(); +for(let y=y0;y>5},${data[i+1]>>5},${data[i+2]>>5}`); n++; } } +console.log('demo card region avg:', [Math.round(rs/n),Math.round(gs/n),Math.round(bs/n)], 'distinct coarse colors:', cols.size); +await b.close(); diff --git a/demodata.mjs b/demodata.mjs new file mode 100644 index 0000000..2c14e87 --- /dev/null +++ b/demodata.mjs @@ -0,0 +1,34 @@ +import puppeteer from 'puppeteer'; +import sharp from 'sharp'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +await p.evaluate(()=>document.querySelector('#sec-shader').scrollIntoView()); +await new Promise(r=>setTimeout(r,600)); +async function probe(key){ + if(key){ await p.click(`[data-shader-switch="${key}"]`); await new Promise(r=>setTimeout(r,900)); } + // снимок canvas напрямую через toDataURL -> png + const dataUrl = await p.evaluate(()=>{ + const cv=document.querySelector('[data-shader-demo] canvas'); + if(!cv) return null; + try { return cv.toDataURL('image/png').slice(0,30); } catch(e){ return 'ERR '+e.message; } + }); + const buf = await p.evaluate(()=>{ + const cv=document.querySelector('[data-shader-demo] canvas'); + return cv ? cv.toDataURL('image/png') : null; + }); + let distinct=0, avg=[0,0,0]; + if(buf){ const b64=buf.split(',')[1]; const {data,info}=await sharp(Buffer.from(b64,'base64')).raw().toBuffer({resolveWithObject:true}); + let n=info.width*info.height,rs=0,gs=0,bs=0; const cols=new Set(); + for(let i=0;i>5},${data[i+1]>>5},${data[i+2]>>5}`); } + avg=[Math.round(rs/n),Math.round(gs/n),Math.round(bs/n)]; distinct=cols.size; } + return { key:key||'mesh', dataUrlHead:dataUrl, avg, distinct }; +} +console.log(JSON.stringify(await probe(),null,0)); +console.log(JSON.stringify(await probe('grain'),null,0)); +console.log(JSON.stringify(await probe('voronoi'),null,0)); +console.log(JSON.stringify(await probe('staticMesh'),null,0)); +await b.close(); diff --git a/demofull.mjs b/demofull.mjs new file mode 100644 index 0000000..36f5522 --- /dev/null +++ b/demofull.mjs @@ -0,0 +1,20 @@ +import puppeteer from 'puppeteer'; +import sharp from 'sharp'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +// прокрутим ровно к карточке через window.scrollTo по абсолютному смещению +const top = await p.evaluate(()=>{ const r=document.querySelector('[data-shader-demo]').getBoundingClientRect(); return r.top+window.scrollY; }); +await p.evaluate((t)=>window.scrollTo(0, t-200), top); +await new Promise(r=>setTimeout(r,900)); +await p.screenshot({path:'/tmp/full2.png'}); +const {data,info}=await sharp('/tmp/full2.png').raw().toBuffer({resolveWithObject:true}); +const rect = await p.evaluate(()=>{ const r=document.querySelector('[data-shader-demo]').getBoundingClientRect(); return {x:r.x,y:r.y,w:r.width,h:r.height}; }); +const x0=Math.max(0,Math.floor(rect.x+20)), y0=Math.max(0,Math.floor(rect.y+20)); +const x1=Math.min(info.width,Math.floor(rect.x+rect.w-20)), y1=Math.min(info.height,Math.floor(rect.y+rect.h-20)); +let rs=0,gs=0,bs=0,n=0; const cols=new Set(); +if(x1>x0&&y1>y0){ for(let y=y0;y>5},${data[i+1]>>5},${data[i+2]>>5}`); n++; } } } +console.log('region avg', n?[Math.round(rs/n),Math.round(gs/n),Math.round(bs/n)]:'empty', 'distinct', cols.size, 'rect', rect, 'imgsize', info.width+'x'+info.height); +await b.close(); diff --git a/demolast.mjs b/demolast.mjs new file mode 100644 index 0000000..3745aab --- /dev/null +++ b/demolast.mjs @@ -0,0 +1,21 @@ +import puppeteer from 'puppeteer'; +import sharp from 'sharp'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +async function probe(key){ + if(key){ await p.click(`[data-shader-switch="${key}"]`); await new Promise(r=>setTimeout(r,1000)); } + const el = await p.$('[data-shader-demo]'); + await el.screenshot({path:'/tmp/demo.png'}); + const {data,info}=await sharp('/tmp/demo.png').raw().toBuffer({resolveWithObject:true}); + let n=info.width*info.height,rs=0,gs=0,bs=0; const cols=new Set(); + for(let i=0;i>5},${data[i+1]>>5},${data[i+2]>>5}`); } + return { key:key||'mesh', avg:[Math.round(rs/n),Math.round(gs/n),Math.round(bs/n)], distinct:cols.size }; +} +console.log(JSON.stringify(await probe())); +console.log(JSON.stringify(await probe('grain'))); +console.log(JSON.stringify(await probe('voronoi'))); +console.log(JSON.stringify(await probe('staticMesh'))); +await b.close(); diff --git a/demook.mjs b/demook.mjs new file mode 100644 index 0000000..20a0423 --- /dev/null +++ b/demook.mjs @@ -0,0 +1,26 @@ +import puppeteer from 'puppeteer'; +import sharp from 'sharp'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +const errs=[]; p.on('console',m=>{if(m.type()==='error')errs.push(m.text())}); p.on('pageerror',e=>errs.push('PE '+e.message)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +await p.evaluate(()=>document.querySelector('#sec-shader').scrollIntoView()); +await new Promise(r=>setTimeout(r,700)); +async function probe(key){ + if(key){ await p.click(`[data-shader-switch="${key}"]`); await new Promise(r=>setTimeout(r,900)); } + const buf = await p.evaluate(()=>{ const cv=document.querySelector('[data-shader-demo] canvas'); return cv?cv.toDataURL('image/png'):null; }); + let avg=[0,0,0], distinct=0; + if(buf){ const {data,info}=await sharp(Buffer.from(buf.split(',')[1],'base64')).raw().toBuffer({resolveWithObject:true}); + let n=info.width*info.height,rs=0,gs=0,bs=0; const cols=new Set(); + for(let i=0;i>5},${data[i+1]>>5},${data[i+2]>>5}`); } + avg=[Math.round(rs/n),Math.round(gs/n),Math.round(bs/n)]; distinct=cols.size; } + return { key:key||'mesh', avg, distinct, hasCanvas:!!buf }; +} +console.log(JSON.stringify(await probe(),null,0)); +console.log(JSON.stringify(await probe('grain'),null,0)); +console.log(JSON.stringify(await probe('voronoi'),null,0)); +console.log(JSON.stringify(await probe('staticMesh'),null,0)); +console.log('errs:', errs); +await b.close(); diff --git a/demopic.mjs b/demopic.mjs new file mode 100644 index 0000000..276700a --- /dev/null +++ b/demopic.mjs @@ -0,0 +1,11 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300,deviceScaleFactor:1}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +await p.evaluate(()=>document.querySelector('#sec-shader').scrollIntoView()); +await new Promise(r=>setTimeout(r,800)); +await p.screenshot({path:'/tmp/sec-shader.png'}); +console.log('saved'); +await b.close(); diff --git a/demovis.mjs b/demovis.mjs new file mode 100644 index 0000000..573a639 --- /dev/null +++ b/demovis.mjs @@ -0,0 +1,28 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1280,height:1200}); +const errs=[]; p.on('console',m=>{if(m.type()==='error')errs.push(m.text())}); p.on('pageerror',e=>errs.push('PE '+e.message)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +await p.evaluate(()=>document.querySelector('[data-shader-demo]').scrollIntoView({block:'center'})); +await new Promise(r=>setTimeout(r,500)); +async function probe(key){ + if(key){ await p.click(`[data-shader-switch="${key}"]`); await new Promise(r=>setTimeout(r,800)); } + return await p.evaluate(()=>{ + const el=document.querySelector('[data-shader-demo]'); + const cv=el.querySelector('canvas'); + if(!cv) return {canvas:false}; + const gl=cv.getContext('webgl2')||cv.getContext('webgl'); + const px=new Uint8Array(4); + // читаем угол (0,0) — там обычно цвет градиента + gl.readPixels(2,2,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px); + return { canvas:true, w:cv.width, h:cv.height, corner:Array.from(px), key: el.dataset.shaderKey }; + }); +} +console.log('mesh:', JSON.stringify(await probe())); +console.log('grain:', JSON.stringify(await probe('grain'))); +console.log('voronoi:', JSON.stringify(await probe('voronoi'))); +console.log('staticMesh:', JSON.stringify(await probe('staticMesh'))); +console.log('errs:', errs); +await b.close(); diff --git a/diag.mjs b/diag.mjs new file mode 100644 index 0000000..ac131bb --- /dev/null +++ b/diag.mjs @@ -0,0 +1,30 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[]; p.on('console',m=>{ if(m.type()==='error'||m.type()==='warning') errs.push(m.type()+': '+m.text()); }); +p.on('pageerror',e=>errs.push('PAGEERR: '+e.message)); +p.on('requestfailed',r=>errs.push('REQFAIL: '+r.url()+' '+r.failure()?.errorText)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +const diag = await p.evaluate(()=>{ + const sec=document.querySelector('#sec-shader'); + const demo=document.querySelector('[data-shader-demo]'); + const canvas=document.querySelector('#sec-shader canvas'); + const sw=document.querySelectorAll('[data-shader-switch]').length; + return { + secExists: !!sec, + secH: sec?Math.round(sec.getBoundingClientRect().height):0, + demoExists: !!demo, + demoH: demo?Math.round(demo.getBoundingClientRect().height):0, + canvasCount: document.querySelectorAll('#sec-shader canvas').length, + swCount: sw, + swVisible: sw?getComputedStyle(document.querySelector('[data-shader-switch]')).display:'n/a', + }; +}); +// проверим доступен ли модуль +const modOk = await p.evaluate(async()=>{ + try { const m = await import('/vendor/paper-shaders/index.js'); return 'OK keys:'+Object.keys(m).length; } + catch(e){ return 'IMPORT ERR: '+e.message; } +}); +console.log(JSON.stringify({diag, modOk, errs: errs.slice(0,10)}, null, 1)); +await b.close(); diff --git a/doccheck.mjs b/doccheck.mjs new file mode 100644 index 0000000..932d6c2 --- /dev/null +++ b/doccheck.mjs @@ -0,0 +1,21 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + // два render (drawArrays инициализируется лениво на 1-м) + m.render(performance.now()); m.render(performance.now()); + const W=m.canvasElement.width, H=m.canvasElement.height; + const buf=new Uint8Array(W*H*4); gl.readPixels(0,0,W,H,gl.RGBA,gl.UNSIGNED_BYTE,buf); + let bright=0, maxc=0; + for(let i=0;i120) bright++; maxc=Math.max(maxc,mx); } + const cntLoc=gl.getUniformLocation(m.program,'u_colorsCount'); const cv=new Float32Array(1); gl.getUniform(m.program,cntLoc,cv); + const c0=new Float32Array(4); gl.getUniform(m.program,gl.getUniformLocation(m.program,'u_colors[0]'),c0); + return { brightPixels: bright, maxChannel: maxc, uColorsCountFloat: cv[0], uColor0: Array.from(c0).map(x=>+x.toFixed(2)) }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/final.mjs b/final.mjs new file mode 100644 index 0000000..3616ac3 --- /dev/null +++ b/final.mjs @@ -0,0 +1,16 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[]; +p.on('pageerror', e=>errs.push('PE '+e.message)); +p.on('console', m=>{ if(m.type()==='error') errs.push('CE '+m.text()); }); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader')?.paperShaderMount; + return { hasMount: !!m, speed: m?.currentSpeed, isVp: m?.isInViewport }; +}); +console.log('MOUNT', JSON.stringify(r)); +console.log('ERRS', errs.slice(0,5)); +await b.close(); diff --git a/final2.mjs b/final2.mjs new file mode 100644 index 0000000..537d022 --- /dev/null +++ b/final2.mjs @@ -0,0 +1,16 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[]; +p.on('pageerror', e=>errs.push('PE '+e.message)); +p.on('console', m=>{ if(m.type()==='error') errs.push('CE '+m.text()); }); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +const a = await p.evaluate(()=>{ const m=document.querySelector('[data-shader-demo] .card-shader')?.paperShaderMount; return m?{speed:m.currentSpeed,frame:m.getCurrentFrame(),inVp:m.isInViewport}:null; }); +await new Promise(r=>setTimeout(r,2000)); +const c = await p.evaluate(()=>{ const m=document.querySelector('[data-shader-demo] .card-shader')?.paperShaderMount; return m?{speed:m.currentSpeed,frame:m.getCurrentFrame(),inVp:m.isInViewport}:null; }); +console.log('A',JSON.stringify(a),'C',JSON.stringify(c)); +console.log('ANIMATING:', c && a && c.frame>a.frame && c.speed>0); +console.log('ERRS', errs.slice(0,5)); +await b.close(); diff --git a/fixcheck.mjs b/fixcheck.mjs new file mode 100644 index 0000000..beb7677 --- /dev/null +++ b/fixcheck.mjs @@ -0,0 +1,12 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,800)); // просто ждём, НЕ скроллим к секции +const a = await p.evaluate(()=>{ const m=document.querySelector('[data-shader-demo] .card-shader')?.paperShaderMount; return m?{frame:m.getCurrentFrame(),speed:m.currentSpeed,inVp:m.isInViewport}:null; }); +await new Promise(r=>setTimeout(r,2000)); +const c = await p.evaluate(()=>{ const m=document.querySelector('[data-shader-demo] .card-shader')?.paperShaderMount; return m?{frame:m.getCurrentFrame(),speed:m.currentSpeed,inVp:m.isInViewport}:null; }); +console.log('A',JSON.stringify(a)); console.log('C',JSON.stringify(c)); +console.log('ANIMATING:', !!c && c.frame>a.frame && c.speed>0); +await b.close(); diff --git a/frametest.mjs b/frametest.mjs new file mode 100644 index 0000000..5d1659f --- /dev/null +++ b/frametest.mjs @@ -0,0 +1,20 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +await p.evaluate(()=>document.querySelector('[data-shader-demo]').scrollIntoView({block:'center'})); +await new Promise(r=>setTimeout(r,500)); +// достанем mount через глобальный хук: ShaderMount сохраняет себя в parentElement.paperShaderMount +const r = await p.evaluate(async()=>{ + const wrap=document.querySelector('[data-shader-demo] .card-shader'); + const m=wrap.paperShaderMount; + if(!m) return {hasMount:false}; + const f1=m.getCurrentFrame(); + await new Promise(r=>setTimeout(r,600)); + const f2=m.getCurrentFrame(); + return { hasMount:true, frame1:f1, frame2:f2, rafRunning: f2>f1, speed: m.speed, inViewport: m.isInViewport, currentSpeed: m.currentSpeed }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/glerr.mjs b/glerr.mjs new file mode 100644 index 0000000..a134c0d --- /dev/null +++ b/glerr.mjs @@ -0,0 +1,19 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,800)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + const errs=[]; + // проверим статус программы и шейдеров + const vs = gl.getProgramParameter(m.program, gl.LINK_STATUS); + // проверим, залит ли canvas хоть чем-то: clearColor потом readPixels + gl.clearColor(1,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT); + const px=new Uint8Array(4); gl.readPixels(10,10,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px); + return { linkStatus: vs, afterClearRed: Array.from(px), glError: gl.getError() }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/glerr2.mjs b/glerr2.mjs new file mode 100644 index 0000000..64431aa --- /dev/null +++ b/glerr2.mjs @@ -0,0 +1,19 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + gl.getError(); // сброс + m.render(performance.now()); + const e = gl.getError(); + const px=new Uint8Array(4); gl.readPixels(360,220,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px); + // проверим a_position location + const aLoc = gl.getAttribLocation(m.program, 'a_position'); + return { glErrorAfterRender: e, centerPx: Array.from(px), aPosLoc: aLoc, clearC: (()=>{const c=new Float32Array(4); gl.getClearColor?.(c); return Array.from(c);})() }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/grid.mjs b/grid.mjs new file mode 100644 index 0000000..caf20d3 --- /dev/null +++ b/grid.mjs @@ -0,0 +1,24 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + const flat=[0.486,0.361,1,1, 0,0.761,0.78,1, 1,0.361,0.541,1, 1,0.702,0.278,1]; + gl.useProgram(m.program); + gl.uniform4fv(gl.getUniformLocation(m.program,'u_colors'), new Float32Array(flat)); + gl.uniform1f(gl.getUniformLocation(m.program,'u_colorsCount'),4); + gl.drawArrays(gl.TRIANGLES,0,6); + const W=m.canvasElement.width,H=m.canvasElement.height; + const pts=[[100,100],[360,100],[620,100],[100,220],[360,220],[620,220],[100,340],[360,340],[620,340]]; + const out=pts.map(([x,y])=>{const px=new Uint8Array(4);gl.readPixels(x,y,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px);return [x,y,Array.from(px)];}); + // макс яркость по всему canvas + const buf=new Uint8Array(W*H*4); gl.readPixels(0,0,W,H,gl.RGBA,gl.UNSIGNED_BYTE,buf); + let maxc=0; for(let i=0;isetTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const keys=['u_time','u_colors','u_colorsCount','u_distortion','u_swirl','u_grainMixer','u_grainOverlay']; + const out={}; + keys.forEach(k=>{ const l=m.uniformLocations[k]; out[k]= l===null?'NULL':(l===undefined?'UNDEF':typeof l); }); + // также посмотрим this.providedUniforms + const pu = m.providedUniforms ? Object.keys(m.providedUniforms) : 'none'; + return { locations: out, providedKeys: pu }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/manual.mjs b/manual.mjs new file mode 100644 index 0000000..a5bae2a --- /dev/null +++ b/manual.mjs @@ -0,0 +1,21 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + const loc0 = gl.getUniformLocation(m.program, 'u_colors'); + const locB = gl.getUniformLocation(m.program, 'u_colors[0]'); + // вручную зададим 4 цвета vec4 + const flat = [0.486,0.361,1,1, 0,0.761,0.78,1, 1,0.361,0.541,1, 1,0.702,0.278,1]; + gl.useProgram(m.program); + if (loc0) gl.uniform4fv(loc0, flat); else if (locB) gl.uniform4fv(locB, flat); + gl.drawArrays(gl.TRIANGLES,0,6); + const px=new Uint8Array(4); gl.readPixels(360,220,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px); + return { loc0null: loc0===null, locBnull: locB===null, centerAfterManual: Array.from(px) }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/matrix.mjs b/matrix.mjs new file mode 100644 index 0000000..13eeae9 --- /dev/null +++ b/matrix.mjs @@ -0,0 +1,23 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());}); +await p.setViewport({width:1100,height:1400}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +await p.evaluate(()=>document.querySelector('#sec-shader-grid')?.scrollIntoView()); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const cards=[...document.querySelectorAll('#sec-shader-grid .neu-card--shader')]; + const info=cards.map(c=>{ + const sh=c.querySelector('.card-shader'); + const m=sh?.paperShaderMount; + const cs=getComputedStyle(c); + const beforeZ=getComputedStyle(c,'::before').zIndex; + return { hasShaderWrap:!!sh, mounted:!!m, speed:m?.currentSpeed, frame:m?Math.round(m.getCurrentFrame()):null, bg:cs.backgroundColor, beforeZ }; + }); + return { count: cards.length, info }; +}); +console.log(JSON.stringify(r,null,1)); +console.log('ERRS', errs.slice(0,5)); +await b.close(); diff --git a/meshcheck.mjs b/meshcheck.mjs new file mode 100644 index 0000000..d16091f --- /dev/null +++ b/meshcheck.mjs @@ -0,0 +1,18 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +await p.evaluate(()=>document.querySelector('#sec-all-shaders')?.scrollIntoView()); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const cards=[...document.querySelectorAll('.card-shader[data-shader]')]; + const info=cards.map(c=>({brand:c.dataset.shader, mounted:!!c.paperShaderMount, frag:c.paperShaderMount?.fragmentShader?.name||null})); + const f0=cards.find(c=>c.dataset.shader==='demo')?.paperShaderMount; + return { count: cards.length, info, demoFrame: f0?Math.round(f0.getCurrentFrame()):null, + demoAnimated: f0? (f0.getCurrentFrame()*0+ (f0.currentSpeed>0)) : null }; +}); +console.log(JSON.stringify(r,null,1)); +console.log('ERRS', errs.slice(0,6)); +await b.close(); diff --git a/mountcheck.mjs b/mountcheck.mjs new file mode 100644 index 0000000..bba84f0 --- /dev/null +++ b/mountcheck.mjs @@ -0,0 +1,12 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[]; p.on('pageerror',e=>errs.push('PE: '+e.message)); p.on('console',m=>{if(m.type()==='error')errs.push('CON: '+m.text());}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +const r = await p.evaluate(()=>{ + const wrap=document.querySelector('[data-shader-demo] .card-shader'); + return { hasMount: !!wrap?.paperShaderMount, scriptLoaded: !!window.__mountLabShaders }; +}); +console.log(JSON.stringify({r, errs}, null, 1)); +await b.close(); diff --git a/pixeltime.mjs b/pixeltime.mjs new file mode 100644 index 0000000..50ba457 --- /dev/null +++ b/pixeltime.mjs @@ -0,0 +1,20 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +await p.evaluate(()=>document.querySelector('[data-shader-demo]').scrollIntoView({block:'center'})); +await new Promise(r=>setTimeout(r,500)); +const r = await p.evaluate(()=>{ + const wrap=document.querySelector('[data-shader-demo] .card-shader'); + const m=wrap.paperShaderMount; const cv=wrap.querySelector('canvas'); + const gl=cv.getContext('webgl2',{preserveDrawingBuffer:true})||cv.getContext('webgl',{preserveDrawingBuffer:true}); + const read=()=>{ const a=new Uint8Array(4); gl.readPixels(cv.width/2|0,cv.height/2|0,1,1,gl.RGBA,gl.UNSIGNED_BYTE,a); return Array.from(a); }; + m.render(0); const t0=read(); + m.render(3000); const t1=read(); + m.render(8000); const t2=read(); + return { t0, t1, t2 }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/probe.mjs b/probe.mjs new file mode 100644 index 0000000..15a3872 --- /dev/null +++ b/probe.mjs @@ -0,0 +1,26 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[],warns=[]; +p.on('pageerror', e=>errs.push('PE '+e.message)); +p.on('requestfailed', r=>errs.push('REQFAIL '+r.url()+' '+r.failure()?.errorText)); +p.on('console', m=>{ const t=m.type(); if(t==='error') errs.push('CE '+m.text()); if(t==='warning') warns.push('W '+m.text()); }); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'networkidle2',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +const info = await p.evaluate(()=>{ + const cs=document.querySelector('[data-shader-demo] .card-shader'); + const m=cs?.paperShaderMount; + const bg=getComputedStyle(cs).backgroundColor; + const canvas=cs?.querySelector('canvas'); + return { + hasWrap: !!cs, hasMount: !!m, + wrapBg: bg, hasCanvas: !!canvas, + canvasW: canvas?.width, canvasH: canvas?.height, + speed: m?.currentSpeed, frame: m?Math.round(m.getCurrentFrame()):null, + }; +}); +console.log('INFO', JSON.stringify(info,null,1)); +console.log('ERRS', errs.slice(0,8)); +console.log('WARNS', warns.slice(0,8)); +await b.close(); diff --git a/probe_tmp.js b/probe_tmp.js new file mode 100644 index 0000000..4ec721e --- /dev/null +++ b/probe_tmp.js @@ -0,0 +1,52 @@ +const puppeteer = require('puppeteer'); + +async function probePage(browser, url, width) { + const page = await browser.newPage(); + await page.setViewport({ width, height: 900, deviceScaleFactor: 1 }); + await page.goto(url, { waitUntil: 'networkidle0' }); + await page.waitForSelector('[data-mini-projects-list] .mini-projects-grid__item', { timeout: 15000 }).catch(()=>{}); + await new Promise(r => setTimeout(r, 700)); + const data = await page.evaluate(() => { + const list = document.querySelector('[data-mini-projects-list]'); + if (!list) return { none: true }; + const inner = list.closest('.section__inner'); + const items = [...list.querySelectorAll('.mini-projects-grid__item')].filter(i => !i.hidden); + const rowCells = (() => { + const w = list.clientWidth; + if (w >= 1180) return 6; + if (w >= 880) return 4; + if (w >= 520) return 2; + return 1; + })(); + return { + listWidth: list.clientWidth, + innerWidth: inner ? inner.clientWidth : null, + rowCells, + itemCount: items.length, + items: items.map(i => ({ + cells: i.getAttribute('data-mp-cells'), + styleW: i.style.width, + x: i.style.transform, + })), + }; + }); + await page.close(); + return data; +} + +(async () => { + const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox','--disable-setuid-sandbox'] }); + const widths = [600, 760, 900, 1024, 1180, 1278, 1356, 1600]; + const caseUrl = 'http://localhost:4175/cases/ai-platform-primo-rpa/'; + const homeUrl = 'http://localhost:4175/'; + for (const w of widths) { + const c = await probePage(browser, caseUrl, w); + console.log(`CASE vw=${w} inner=${c.innerWidth} rowCells=${c.rowCells} items=${c.itemCount} ` + JSON.stringify(c.items)); + } + console.log('---'); + for (const w of widths) { + const h = await probePage(browser, homeUrl, w); + console.log(`HOME vw=${w} inner=${h.innerWidth} rowCells=${h.rowCells} items=${h.itemCount} ` + JSON.stringify(h.items)); + } + await browser.close(); +})().catch(e => { console.error(e); process.exit(1); }); diff --git a/public/en/index.html b/public/en/index.html index b687a3b..304e316 100644 --- a/public/en/index.html +++ b/public/en/index.html @@ -104,7 +104,7 @@ - + diff --git a/pulse.mjs b/pulse.mjs new file mode 100644 index 0000000..da19ac2 --- /dev/null +++ b/pulse.mjs @@ -0,0 +1,20 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());}); +await p.setViewport({width:1100,height:1500}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +await p.evaluate(()=>document.querySelector('#sec-all-shaders')?.scrollIntoView()); +await new Promise(r=>setTimeout(r,1500)); +const r = await p.evaluate(()=>{ + const cells=[...document.querySelectorAll('[data-shader-cell="pulsingBorder"]')]; + const info=cells.map(c=>{const m=c.querySelector('.card-shader')?.paperShaderMount; return {mounted:!!m, speed:m?.currentSpeed, frame:m?Math.round(m.getCurrentFrame()):null};}); + // проверим, что noise-текстура задалась + const m0=cells[0]?.querySelector('.card-shader')?.paperShaderMount; + const texLoc=m0?m0.gl.getUniform(m0.program, m0.gl.getUniformLocation(m0.program,'u_noiseTexture')):null; + return { cellCount: cells.length, info, noiseTexSet: texLoc!==null && texLoc!==undefined }; +}); +console.log(JSON.stringify(r,null,1)); +console.log('ERRS', errs.slice(0,8)); +await b.close(); diff --git a/pure.mjs b/pure.mjs new file mode 100644 index 0000000..b032584 --- /dev/null +++ b/pure.mjs @@ -0,0 +1,14 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +await p.evaluate(()=>document.querySelector('[data-shader-demo]').scrollIntoView({block:'center'})); +await new Promise(r=>setTimeout(r,500)); +const a = await p.evaluate(()=>{ const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; return {frame:m.getCurrentFrame(), speed:m.currentSpeed, inVp:m.isInViewport, cs:m.currentSpeed}; }); +await new Promise(r=>setTimeout(r,2000)); +const c = await p.evaluate(()=>{ const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; return {frame:m.getCurrentFrame(), speed:m.currentSpeed, inVp:m.isInViewport}; }); +console.log('A', JSON.stringify(a)); console.log('C', JSON.stringify(c)); +console.log('FRAME GROWS:', c.frame>a.frame, ' (', a.frame.toFixed(1), '->', c.frame.toFixed(1), ')'); +await b.close(); diff --git a/raf.mjs b/raf.mjs new file mode 100644 index 0000000..ad5514f --- /dev/null +++ b/raf.mjs @@ -0,0 +1,32 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +await p.evaluate(()=>document.querySelector('[data-shader-demo]').scrollIntoView({block:'center'})); +await new Promise(r=>setTimeout(r,300)); +// достанем mount из WeakMap нельзя, но проверим через глобальный хук: переопределим requestAnimationFrame до загрузки? уже поздно. +// Вместо этого — вручную создадим ShaderMount и проверим frame рост + readPixels внутри rAF +const res = await p.evaluate(async()=>{ + const mod = await import('/vendor/paper-shaders/index.js'); + const wrap = document.querySelector('[data-shader-demo] .card-shader'); + const cv = wrap.querySelector('canvas'); + // создадим свой mount того же шейдера в НОВЫЙ контейнер чтобы не конфликтовать + const host = document.createElement('div'); host.style.cssText='position:fixed;left:0;top:0;width:200px;height:120px;z-index:9999'; + document.body.appendChild(host); + const m = new mod.ShaderMount(host, mod.meshGradientFragmentShader, { u_colors:['#7c5cff','#00c2c7','#ff5c8a'], u_colorsCount:3, u_distortion:0.6, u_swirl:0.4, u_grainMixer:0.08, u_grainOverlay:0.04 }, {preserveDrawingBuffer:true}, 0.2); + await new Promise(r=>setTimeout(r,500)); + const frame1 = m.getCurrentFrame(); + await new Promise(r=>setTimeout(r,500)); + const frame2 = m.getCurrentFrame(); + // читаем пиксель через свой canvas с preserveDrawingBuffer + const c2 = host.querySelector('canvas'); + const gl = c2.getContext('webgl', {preserveDrawingBuffer:true}) || c2.getContext('webgl2',{preserveDrawingBuffer:true}); + let px='n/a'; + if(gl){ const a=new Uint8Array(4); gl.readPixels(10,10,1,1,gl.RGBA,gl.UNSIGNED_BYTE,a); px=Array.from(a); } + m.dispose(); host.remove(); + return { frame1, frame2, animated: frame2>frame1, px }; +}); +console.log(JSON.stringify(res,null,1)); +await b.close(); diff --git a/readback.mjs b/readback.mjs new file mode 100644 index 0000000..b4a46c7 --- /dev/null +++ b/readback.mjs @@ -0,0 +1,27 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,800)); +const r = await p.evaluate(async ()=>{ + const mod = await import('/vendor/paper-shaders/index.js'); + const { ShaderMount, meshGradientFragmentShader, getShaderColorFromString } = mod; + const host = document.createElement('div'); host.style.cssText='width:300px;height:200px;position:relative'; + document.body.appendChild(host); + const colors = ['#7c5cff','#00c2c7','#ff5c8a','#ffb347'].map(getShaderColorFromString); + const uniforms = { u_colors: colors, u_colorsCount: colors.length, u_distortion:1.2, u_swirl:0.8, u_grainMixer:0.08, u_grainOverlay:0.04 }; + const m = new ShaderMount(host, meshGradientFragmentShader, uniforms, { preserveDrawingBuffer:true, antialias:true }, 1.0); + const gl=m.gl, pr=m.program; + const f=(n)=>{const l=gl.getUniformLocation(pr,n);const v=new Float32Array(4);gl.getUniform(pr,l,v);return Array.from(v).map(x=>+x.toFixed(3));}; + const fi=(n)=>{const l=gl.getUniformLocation(pr,n);const v=new Float32Array(1);gl.getUniform(pr,l,v);return v[0];}; + return { + u_swirl: fi('u_swirl'), + u_distortion: fi('u_distortion'), + u_colorsCount: fi('u_colorsCount'), + u_color0: f('u_colors[0]'), + u_color1: f('u_colors[1]'), + }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/readu.mjs b/readu.mjs new file mode 100644 index 0000000..3340e99 --- /dev/null +++ b/readu.mjs @@ -0,0 +1,22 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + const out=[]; + for(let i=0;i<4;i++){ + const loc=gl.getUniformLocation(m.program,'u_colors['+i+']'); + const v=new Float32Array(4); + gl.getUniform(m.program,loc,v); + out.push(Array.from(v).map(x=>+x.toFixed(3))); + } + const cnt=gl.getUniformLocation(m.program,'u_colorsCount'); + const cv=new Int32Array(1); gl.getUniform(m.program,cnt,cv); + return { uColors: out, count: cv[0] }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/real.mjs b/real.mjs new file mode 100644 index 0000000..bcb5aae --- /dev/null +++ b/real.mjs @@ -0,0 +1,16 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +// принудительно рендерим два кадра с разным временем и берём toDataURL +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const cv=m.canvasElement; + m.render(0); const a=cv.toDataURL('image/png'); + m.render(8000); const c=cv.toDataURL('image/png'); + return { lenA:a.length, lenC:c.length, same: a===c, headA:a.slice(0,40) }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/rendercheck.mjs b/rendercheck.mjs new file mode 100644 index 0000000..c69ceed --- /dev/null +++ b/rendercheck.mjs @@ -0,0 +1,18 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());}); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + m.render(performance.now()); m.render(performance.now()); + const W=m.canvasElement.width,H=m.canvasElement.height; const buf=new Uint8Array(W*H*4); gl.readPixels(0,0,W,H,gl.RGBA,gl.UNSIGNED_BYTE,buf); + let bright=0,maxc=0; for(let i=0;i120)bright++; maxc=Math.max(maxc,mx);} + return { brightPixels: bright, maxChannel: maxc }; +}); +console.log(JSON.stringify(r,null,1)); +console.log('ERRS', errs.slice(0,5)); +await b.close(); diff --git a/res.mjs b/res.mjs new file mode 100644 index 0000000..6a13cfa --- /dev/null +++ b/res.mjs @@ -0,0 +1,19 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl, pr=m.program; + const gf=(n)=>gl.getUniform(pr, gl.getUniformLocation(pr,n)); + return { + u_resolution: gf('u_resolution'), + u_pixelRatio: gf('u_pixelRatio'), + linkStatus: gl.getProgramParameter(pr, gl.LINK_STATUS), + vsLog: gl.getShaderInfoLog(gl.getShader(gl.VERTEX_SHADER)), + }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/res2.mjs b/res2.mjs new file mode 100644 index 0000000..9bb4910 --- /dev/null +++ b/res2.mjs @@ -0,0 +1,14 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl, pr=m.program; + const gf=(n)=>gl.getUniform(pr, gl.getUniformLocation(pr,n)); + return { u_resolution: gf('u_resolution'), u_pixelRatio: gf('u_pixelRatio'), canvasW: m.canvasElement.width, canvasH: m.canvasElement.height }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/selfraf.mjs b/selfraf.mjs new file mode 100644 index 0000000..fed592d --- /dev/null +++ b/selfraf.mjs @@ -0,0 +1,14 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +const errs=[]; p.on('pageerror',e=>errs.push('PE '+e.message)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +// берём toDataURL двух кадров с интервалом — мой rAF должен сдвинуть currentFrame +const a = await p.evaluate(()=>{ const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; return m.canvasElement.toDataURL('image/png').length; }); +await new Promise(r=>setTimeout(r,1500)); +const c = await p.evaluate(()=>{ const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; return m.canvasElement.toDataURL('image/png').length; }); +console.log('dataURL length t1:', a, ' t2:', c, ' CHANGED:', a!==c); +console.log('errs', errs.slice(0,3)); +await b.close(); diff --git a/setframe.mjs b/setframe.mjs new file mode 100644 index 0000000..d4f41bd --- /dev/null +++ b/setframe.mjs @@ -0,0 +1,15 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const cv=m.canvasElement; + m.setFrame(0); const a=cv.toDataURL('image/png'); + m.setFrame(10000); const c=cv.toDataURL('image/png'); + return { same: a===c, lenA:a.length, lenC:c.length }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/setu.mjs b/setu.mjs new file mode 100644 index 0000000..7e29a0f --- /dev/null +++ b/setu.mjs @@ -0,0 +1,17 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + m.setUniforms({ u_colorsCount: 4, u_colors: [[0.486,0.361,1,1],[0,0.761,0.78,1],[1,0.361,0.541,1],[1,0.702,0.278,1]] }); + gl.drawArrays(gl.TRIANGLES,0,6); + const px=new Uint8Array(4); gl.readPixels(360,220,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px); + const cnt=gl.getUniformLocation(m.program,'u_colorsCount'); const cv=new Int32Array(1); gl.getUniform(m.program,cnt,cv); + return { center:Array.from(px), count: cv[0] }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/setu2.mjs b/setu2.mjs new file mode 100644 index 0000000..198039e --- /dev/null +++ b/setu2.mjs @@ -0,0 +1,21 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,800)); +const r = await p.evaluate(async ()=>{ + const mod = await import('/vendor/paper-shaders/index.js'); + const { ShaderMount, meshGradientFragmentShader, getShaderColorFromString } = mod; + const host = document.createElement('div'); host.style.cssText='width:300px;height:200px;position:relative'; + document.body.appendChild(host); + const colors = ['#7c5cff','#00c2c7','#ff5c8a','#ffb347'].map(getShaderColorFromString); + const m = new ShaderMount(host, meshGradientFragmentShader, {}, { preserveDrawingBuffer:true, antialias:true }, 1.0); + m.setUniforms({ u_colors: colors, u_colorsCount: colors.length, u_distortion:1.2, u_swirl:0.8 }); + const gl=m.gl, pr=m.program; + const fi=(n)=>{const l=gl.getUniformLocation(pr,n);const v=new Float32Array(1);gl.getUniform(pr,l,v);return v[0];}; + const f=(n)=>{const l=gl.getUniformLocation(pr,n);const v=new Float32Array(4);gl.getUniform(pr,l,v);return Array.from(v).map(x=>+x.toFixed(3));}; + return { u_swirl: fi('u_swirl'), u_colorsCount: fi('u_colorsCount'), u_color0: f('u_colors[0]') }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/shadow.mjs b/shadow.mjs new file mode 100644 index 0000000..cefb31e --- /dev/null +++ b/shadow.mjs @@ -0,0 +1,24 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1400}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +await p.evaluate(()=>document.querySelector('#sec-shader-grid')?.scrollIntoView()); +await new Promise(r=>setTimeout(r,800)); +const r = await p.evaluate(()=>{ + const shaderCard=document.querySelector('#sec-shader-grid .neu-card--shader'); + const plainCard=document.querySelector('#sec-shader-grid .neu-card:not(.neu-card--shader)'); + const cs=getComputedStyle(shaderCard); + const after=getComputedStyle(shaderCard,'::after'); + const plain=getComputedStyle(plainCard); + const countShadows=(s)=>s.split(/,(?![^()]*\))/).filter(x=>x.trim()).length; + return { + shaderBoxShadow: cs.boxShadow.slice(0,80)+'...', shaderShadowCount: countShadows(cs.boxShadow), + afterBoxShadow: after.boxShadow.slice(0,80)+'...', afterShadowCount: countShadows(after.boxShadow), + afterZ: after.zIndex, beforeZ: getComputedStyle(shaderCard,'::before').zIndex, + plainShadowCount: countShadows(plain.boxShadow), + }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/shchk.mjs b/shchk.mjs new file mode 100644 index 0000000..43dcc12 --- /dev/null +++ b/shchk.mjs @@ -0,0 +1,24 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const errs=[]; p.on('console',m=>{if(m.type()==='error')errs.push(m.text())}); p.on('pageerror',e=>errs.push('PE '+e.message)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +const info = await p.evaluate(()=>{ + const hosts=[...document.querySelectorAll('#sec-shader .card-shader-host')]; + return hosts.map(h=>{ + const cv=h.querySelector('canvas'); + return { sel:h.dataset.shader, hasCanvas:!!cv, w:cv?.width, h:cv?.height, mounted:h.dataset.mounted }; + }); +}); +// снимем пиксель чтобы убедиться что шейдер реально рисует (не пустой canvas) +const pix = await p.evaluate(()=>{ + const cv=document.querySelector('#sec-shader .card-shader-host canvas'); + if(!cv) return 'no-canvas'; + const gl=cv.getContext('webgl2')||cv.getContext('webgl'); + if(!gl) return 'no-gl'; + const px=new Uint8Array(4); gl.readPixels(Math.floor(cv.width/2),Math.floor(cv.height/2),1,1,gl.RGBA,gl.UNSIGNED_BYTE,px); + return Array.from(px).join(','); +}); +console.log(JSON.stringify({info, centerPixel:pix, errs}, null, 1)); +await b.close(); diff --git a/shclick.mjs b/shclick.mjs new file mode 100644 index 0000000..bf112b3 --- /dev/null +++ b/shclick.mjs @@ -0,0 +1,22 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +await p.setViewport({width:1280,height:1000}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +// подпишемся на клик по ссылке внутри страницы +await p.evaluate(()=>{ + window.__clicked=false; + document.querySelector('#sec-shader a.contact-card').addEventListener('click', ()=>{ window.__clicked=true; }, true); +}); +const link = await p.$('#sec-shader a.contact-card'); +const box = await link.boundingBox(); +await p.mouse.click(box.x+box.width/2, box.y+box.height/2); +await new Promise(r=>setTimeout(r,300)); +const clicked = await p.evaluate(()=>window.__clicked); +// hover тоже +await p.mouse.move(box.x+box.width/2, box.y+box.height/2); +await new Promise(r=>setTimeout(r,400)); +const hover = await p.evaluate(()=>getComputedStyle(document.querySelector('#sec-shader a.contact-card')).transform); +console.log(JSON.stringify({clicked, hover},null,1)); +await b.close(); diff --git a/shelem.mjs b/shelem.mjs new file mode 100644 index 0000000..12eb7bb --- /dev/null +++ b/shelem.mjs @@ -0,0 +1,24 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +await p.setViewport({width:1280,height:1000}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const card=document.querySelector('#sec-shader a.contact-card'); + const rect=card.getBoundingClientRect(); + const x=rect.x+rect.width/2, y=rect.y+rect.height/2; + const el=document.elementFromPoint(x,y); + const path=[]; + let e=el; while(e && path.length<5){ path.push(e.className||e.tagName); e=e.parentElement; } + // что у canvas + const cv=card.querySelector('canvas'); + return { + topEl: el.className||el.tagName, + path, + canvasPE: cv?getComputedStyle(cv).pointerEvents:'', canvasZ: cv?getComputedStyle(cv).zIndex:'', + cardZ: getComputedStyle(card).zIndex, cardPos: getComputedStyle(card).position, + }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/shelem2.mjs b/shelem2.mjs new file mode 100644 index 0000000..c95d14b --- /dev/null +++ b/shelem2.mjs @@ -0,0 +1,24 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +await p.setViewport({width:1280,height:1000}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +await p.evaluate(()=>document.querySelector('#sec-shader').scrollIntoView()); +await new Promise(r=>setTimeout(r,400)); +await p.evaluate(()=>{ + window.__clicked=false; + const c=document.querySelector('#sec-shader a.contact-card'); + c.addEventListener('click', ()=>{ window.__clicked=true; }, true); +}); +const box = await p.$eval('#sec-shader a.contact-card', el=>{const r=el.getBoundingClientRect();return {x:r.x,y:r.y,w:r.width,h:r.height};}); +const x=box.x+box.w/2, y=box.y+box.h/2; +await p.mouse.move(x,y); +await new Promise(r=>setTimeout(r,400)); +const hover = await p.evaluate(()=>getComputedStyle(document.querySelector('#sec-shader a.contact-card')).transform); +await p.mouse.click(x,y); +await new Promise(r=>setTimeout(r,300)); +const clicked = await p.evaluate(()=>window.__clicked); +const top = await p.evaluate((x,y)=>{const e=document.elementFromPoint(x,y);return e?(e.className||e.tagName):'null';}, x, y); +console.log(JSON.stringify({hover, clicked, topElAtCenter: top},null,1)); +await b.close(); diff --git a/shelem3.mjs b/shelem3.mjs new file mode 100644 index 0000000..6191fa6 --- /dev/null +++ b/shelem3.mjs @@ -0,0 +1,24 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +await p.setViewport({width:1280,height:1000}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +const meta = await p.evaluate(()=>{ + const c=document.querySelector('#sec-shader a.contact-card'); + const r=c.getBoundingClientRect(); + return { docH: document.body.scrollHeight, viewH: window.innerHeight, + cardTop: r.top, cardBottom: r.bottom, cardRect: {x:r.x,y:r.y,w:r.width,h:r.height} }; +}); +console.log('before scroll', JSON.stringify(meta)); +await p.evaluate(()=>document.querySelector('#sec-shader a.contact-card').scrollIntoView({block:'center'})); +await new Promise(r=>setTimeout(r,500)); +const after = await p.evaluate(()=>{ + const c=document.querySelector('#sec-shader a.contact-card'); + const r=c.getBoundingClientRect(); + const cx=r.x+r.width/2, cy=r.y+r.height/2; + const e=document.elementFromPoint(cx,cy); + return { cardTop: r.top, cardBottom: r.bottom, topEl: e?(e.className||e.tagName):'null', cx, cy }; +}); +console.log('after scroll', JSON.stringify(after)); +await b.close(); diff --git a/shfin.mjs b/shfin.mjs new file mode 100644 index 0000000..5b80fbe --- /dev/null +++ b/shfin.mjs @@ -0,0 +1,16 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +const errs=[]; p.on('console',m=>{if(m.type()==='error')errs.push(m.text())}); p.on('pageerror',e=>errs.push('PE '+e.message)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const card=document.querySelector('#sec-shader .card-shader-host .contact-card'); + const cs=getComputedStyle(card); + const wrap=document.querySelector('#sec-shader .card-shader'); + return { boxShadow: cs.boxShadow.slice(0,40), glow: cs.backgroundImage.slice(0,30), + shaderLayer: getComputedStyle(wrap).position, shaderZ: getComputedStyle(wrap).zIndex, + contentZ: getComputedStyle(card.querySelector('.contact-card__body')).zIndex }; +}); +console.log(JSON.stringify({r, errs},null,1)); +await b.close(); diff --git a/shfinal.mjs b/shfinal.mjs new file mode 100644 index 0000000..ab5bf6a --- /dev/null +++ b/shfinal.mjs @@ -0,0 +1,23 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +await p.setViewport({width:1280,height:1000}); +const errs=[]; p.on('console',m=>{if(m.type()==='error')errs.push(m.text())}); p.on('pageerror',e=>errs.push('PE '+e.message)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +const link = await p.$('#sec-shader a.contact-card'); +await link.hover(); +await new Promise(r=>setTimeout(r,400)); +const hover = await p.evaluate(()=>getComputedStyle(document.querySelector('#sec-shader a.contact-card')).transform); +let popup='none'; +p.once('popup', ()=>{ popup='popup'; }); +await link.click({delay:50}); +await new Promise(r=>setTimeout(r,300)); +const geo = await p.evaluate(()=>{ + const li=document.querySelector('#sec-shader li'); const card=li.querySelector('.contact-card'); + const cv=card.querySelector('canvas'); + return { liH:Math.round(li.getBoundingClientRect().height), cardH:Math.round(card.getBoundingClientRect().height), + canvasInCard: card.contains(cv), canvasPE: cv?getComputedStyle(cv).pointerEvents:'none' }; +}); +console.log(JSON.stringify({hover, popup, geo, errs},null,1)); +await b.close(); diff --git a/shfix.mjs b/shfix.mjs new file mode 100644 index 0000000..4d69513 --- /dev/null +++ b/shfix.mjs @@ -0,0 +1,26 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +await p.setViewport({width:1280,height:1000}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const info = await p.evaluate(()=>{ + const host=document.querySelector('#sec-shader .card-shader-host'); + const li=host; const card=host.querySelector('.contact-card'); const canvas=host.querySelector('canvas'); + const lr=getComputedStyle(li), cr=getComputedStyle(card), cvr=getComputedStyle(canvas); + return { + liDisplay: lr.display, liPosition: lr.position, liW: li.getBoundingClientRect().width, + cardW: card.getBoundingClientRect().width, cardH: card.getBoundingClientRect().height, + canvasPointer: cvr.pointerEvents, canvasZ: cvr.zIndex, canvasInset: cvr.position, + cardPointer: cr.pointerEvents, + }; +}); +console.log(JSON.stringify(info,null,1)); +// тест клика по ссылке +const link = await p.$('#sec-shader a.contact-card'); +const box = await link.boundingBox(); +await p.mouse.click(box.x+box.width/2, box.y+box.height/2); +await new Promise(r=>setTimeout(r,300)); +const url = p.url(); +console.log('after click url:', url, '(same page expected, external link opens new tab)'); +await b.close(); diff --git a/shlog.mjs b/shlog.mjs new file mode 100644 index 0000000..14a5bca --- /dev/null +++ b/shlog.mjs @@ -0,0 +1,21 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +const logs=[]; +p.on('console', m=>logs.push('['+m.type()+'] '+m.text())); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + // проверим компиляцию шейдеров напрямую + const vs=gl.getParameter(gl.VERTEX_SHADER); + // считаем context lost + const lost = gl.isContextLost(); + // размер drawing buffer + const dbw=gl.drawingBufferWidth, dbh=gl.drawingBufferHeight; + return { contextLost: lost, dbW: dbw, dbH: dbh, glError: gl.getError() }; +}); +console.log('R', JSON.stringify(r)); +console.log('LOGS:\n'+logs.slice(0,15).join('\n')); +await b.close(); diff --git a/shot.mjs b/shot.mjs new file mode 100644 index 0000000..13237d1 --- /dev/null +++ b/shot.mjs @@ -0,0 +1,12 @@ +import puppeteer from 'puppeteer'; +import fs from 'fs'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1500,deviceScaleFactor:1}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1800)); +const el = await p.$('#sec-shader-grid'); +const box = await el.boundingBox(); +await el.screenshot({ path: '/tmp/matrix.png' }); +console.log('saved', box?.width, box?.height); +await b.close(); diff --git a/shot_tmp.js b/shot_tmp.js new file mode 100644 index 0000000..50c9c02 --- /dev/null +++ b/shot_tmp.js @@ -0,0 +1,14 @@ +const puppeteer = require('puppeteer'); +(async () => { + const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox','--disable-setuid-sandbox'] }); + const page = await browser.newPage(); + await page.setViewport({ width: 1278, height: 900, deviceScaleFactor: 1 }); + await page.goto('http://localhost:4175/cases/ai-platform-primo-rpa/', { waitUntil: 'networkidle0' }); + await page.waitForSelector('[data-mini-projects-list]'); + const el = await page.$('[data-mini-projects-list]'); + await el.evaluate(e => e.scrollIntoView({block:'center'})); + await new Promise(r => setTimeout(r, 600)); + await el.screenshot({ path: '/tmp/related_1278.png' }); + await browser.close(); + console.log('shot saved'); +})().catch(e => { console.error(e); process.exit(1); }); diff --git a/shotcmp.mjs b/shotcmp.mjs new file mode 100644 index 0000000..e434031 --- /dev/null +++ b/shotcmp.mjs @@ -0,0 +1,16 @@ +import puppeteer from 'puppeteer'; +import crypto from 'crypto'; +import fs from 'fs'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +await p.evaluate(()=>document.querySelector('[data-shader-demo]').scrollIntoView({block:'center'})); +await new Promise(r=>setTimeout(r,500)); +const h1 = await p.screenshot({encoding:'binary'}); await fs.promises.writeFile('/tmp/s1.png', h1); +await new Promise(r=>setTimeout(r,1500)); +const h2 = await p.screenshot({encoding:'binary'}); await fs.promises.writeFile('/tmp/s2.png', h2); +const m1=crypto.createHash('md5').update(h1).digest('hex'); const m2=crypto.createHash('md5').update(h2).digest('hex'); +console.log('bytes', h1.length, h2.length, 'md5equal', m1===m2); +await b.close(); diff --git a/shreal.mjs b/shreal.mjs new file mode 100644 index 0000000..b26c5d4 --- /dev/null +++ b/shreal.mjs @@ -0,0 +1,16 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +await p.setViewport({width:1280,height:1000}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +await p.evaluate(()=>{ window.__clicked=false; const c=document.querySelector('#sec-shader a.contact-card'); c.addEventListener('click',()=>window.__clicked=true,true); }); +const link = await p.$('#sec-shader a.contact-card'); +await link.hover(); +await new Promise(r=>setTimeout(r,400)); +const hover = await p.evaluate(()=>getComputedStyle(document.querySelector('#sec-shader a.contact-card')).transform); +await link.click({delay:50}); +await new Promise(r=>setTimeout(r,300)); +const clicked = await p.evaluate(()=>window.__clicked); +console.log(JSON.stringify({hover, clicked},null,1)); +await b.close(); diff --git a/shver.mjs b/shver.mjs new file mode 100644 index 0000000..6a621f8 --- /dev/null +++ b/shver.mjs @@ -0,0 +1,32 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader'] }); +const p = await b.newPage(); +await p.setViewport({width:1280,height:1000}); +const errs=[]; p.on('console',m=>{if(m.type()==='error')errs.push(m.text())}); p.on('pageerror',e=>errs.push('PE '+e.message)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +const geo = await p.evaluate(()=>{ + const li=document.querySelector('#sec-shader li'); + const card=li.querySelector('.contact-card'); + const canvas=li.querySelector('canvas'); + return { + liH: Math.round(li.getBoundingClientRect().height), + cardH: Math.round(card.getBoundingClientRect().height), + canvasInCard: canvas ? card.contains(canvas) : false, + canvasPE: canvas ? getComputedStyle(canvas).pointerEvents : 'no-canvas', + cardPE: getComputedStyle(card).pointerEvents, + }; +}); +// hover + click test +const link = await p.$('#sec-shader a.contact-card'); +const box = await link.boundingBox(); +await p.mouse.move(box.x+box.width/2, box.y+box.height/2); +await new Promise(r=>setTimeout(r,200)); +const hoverTransform = await p.evaluate(()=>getComputedStyle(document.querySelector('#sec-shader a.contact-card')).transform); +// click -> should open new tab (we block navigation, just check no error) +const newTarget = await Promise.race([ + new Promise(res=>p.once('popup', t=>res('popup'))), + new Promise(res=>setTimeout(()=>res('none'), 800)) +]); +console.log(JSON.stringify({geo, hoverTransform, newTarget, errs}, null, 1)); +await b.close(); diff --git a/shvis.mjs b/shvis.mjs new file mode 100644 index 0000000..59decf7 --- /dev/null +++ b/shvis.mjs @@ -0,0 +1,17 @@ +import puppeteer from 'puppeteer'; +import sharp from 'sharp'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1280,height:1400,deviceScaleFactor:1}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,2000)); +const el = await p.$('#sec-shader'); +await el.screenshot({ path: '/tmp/shader-sec.png' }); +// analyse average + stddev of color +const {data,info} = await sharp('/tmp/shader-sec.png').raw().toBuffer({resolveWithObject:true}); +let n=info.width*info.height, rs=0,gs=0,bs=0; const cols=new Set(); +for(let i=0;i>4},${data[i+1]>>4},${data[i+2]>>4}`); } +console.log('avg rgb', Math.round(rs/n),Math.round(gs/n),Math.round(bs/n)); +console.log('distinct coarse colors sampled:', cols.size); +await b.close(); diff --git a/size.mjs b/size.mjs new file mode 100644 index 0000000..b937708 --- /dev/null +++ b/size.mjs @@ -0,0 +1,19 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const wrap=m.parentElement; + return { + canvasW:m.canvasElement.width, canvasH:m.canvasElement.height, + canvasCSS: getComputedStyle(m.canvasElement).width+'x'+getComputedStyle(m.canvasElement).height, + parentW:m.parentWidth, parentH:m.parentHeight, + wrapRect: (()=>{const r=wrap.getBoundingClientRect();return {w:r.width,h:r.height};})(), + hasRO: !!m.resizeObserver, + }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/src/pages/index.en.html b/src/pages/index.en.html index 5b741fe..6cf966a 100644 --- a/src/pages/index.en.html +++ b/src/pages/index.en.html @@ -11,7 +11,7 @@ {{> head-links}} - + {{> header-home}} {{> fab-stack}}
diff --git a/texts/cases/card-columns-obsidian.md b/texts/cases/card-columns-obsidian.md new file mode 100644 index 0000000..a71e77f --- /dev/null +++ b/texts/cases/card-columns-obsidian.md @@ -0,0 +1,116 @@ +# Card Columns — плагин-вид для Obsidian Bases + +- **Источник:** `public/cases/card-columns-obsidian/index.html` +- **eyebrow:** Obsidian Bases · Plugin +- **meta description:** Кейс: Card Columns, плагин-вид для Obsidian Bases. Показывает заметки как карточки, разложенные по колонкам, с фильтрацией тегов по И/ИЛИ. + +--- + +**Заметка по путям к медиа:** файлы кладутся в `public/assets/card-columns-obsidian/`, в разметке страницы пути `../assets/card-columns-obsidian/файл.webp`. Форматы: WebP (изображения, постеры), WebM + MP4 (видео). + +## Brief (блок метаданных на странице) + +- **Роль:** Продуктовый дизайн и разработка +- **Платформа:** Obsidian Bases +- **Статус:** Рабочий плагин, open source (MIT) +- **Команда:** Соло, пет-проект + +## Введение + +В Obsidian появились Bases, отдельный тип файла, который показывает заметки не текстом, а в разных видах: таблицей, карточками, деревом. Стандартный вид карточек мне не зашёл: он плоский, без группировки, и в нём не разобраться, когда заметок сотни. Я хотел видеть их раскладку по колонкам, как канбан, но без лишнего веса. Так появился плагин-вид Card Columns. + +## Контекст + +Bases, это не заметка и не отдельная база данных. Это запрос к vault-у: выбираешь заметки по фильтру, и фреймворк рисует их в выбранном виде. У самого Obsidian есть встроенный вид карточек, но он одноплановый: нет группировки по свойствам, нет нормальной фильтрации. Я решил сделать свой вид, который встраивается в переключатель Bases наравне со штатными. + +Главное ограничение было в самом фреймворке: плагин не хранит данные, а рисует то, что выдал Bases-запрос (Sort, Filter, Properties, Search из тулбара). Значит, вся логика выборки остаётся на Bases, а моя задача, только представление и навигация по нему. + +## Задача + +Спроектировать и собрать view-плагин для Obsidian Bases: заметки из запроса показываются карточками, разложенными по колонкам, а по колонкам легко ориентироваться и отбирать нужное. Вид должен жить в переключателе Bases рядом со штатными и не ломать встроенный тулбар (сортировку, фильтр, поиск). + +> **Callout (📌):** Главный риск, сделать вид, который конфликтует со штатным тулбаром Bases. Тогда пользователь теряет сортировку и поиск, ради которых Bases и берут. Этого нельзя было допустить. + +## Решение + +Плагин добавляет в Bases вид «Columns». Карточки строятся из результатов Bases-запроса, группировка задаётся свойством, а отбор, кликабельными тегами. + +### Группировка по любому свойству + +Колонки формируются из Group by в тулбаре Bases, по тегу, статусу, категории или любому frontmatter-свойству. Меняешь свойство в запросе, и доска пересобирается сама. Так одна и та же база закрывает разные разрезы: проекты по статусу, заметки по тегам, задачи по приоритету. + +### Фильтр тегов по И и ИЛИ + +Это то, ради чего плагин и задумывался. Под тулбаром, пилюли тегов, по клику они отсеивают карточки по всем колонкам сразу: + +- Левый клик по тегу, показать только его (сбрасывает остальные фильтры). +- Левый клик по следующему тегу, показать заметки с ЛЮБЫМ из выбранных (режим ИЛИ). +- Ctrl+Клик или правый клик, переключить тег, не сбрасывая выбор. +- Кнопка OR/AND, переключить между «любой из» и «все перечисленные» для нескольких тегов. +- All, снять все фильтры. + +Связка «ИЛИ-добавление левым кликом + точный toggle через Ctrl» получилась интуитивной: быстрый отбор не сбивает то, что уже выбрано. Альтернативу, вынести фильтр в модалку, отверг сразу: лишний клик и потеря контекста того, что уже отфильтровано. + +### Карточки и обложки + +По каждой заметке рисуются свойства как чипы, текст, теги, ссылки, булевы, даты. Можно показать обложку: из первой картинки в заметке или из отдельного свойства. У обложки полный контроль, соотношение сторон (Auto, 1:1, 3:2, 4:3, 16:9), рамка, вписывание (cover/contain), ориентация и место на карточке (над заголовком, под ним, после всего). + +### Что заведено в настройки + +Всю внешнюю вариативность я вынел в gear-меню, разбив на четыре секции, чтобы вид подстраивался под разные базы, а не под мой единственный сценарий. + +**General** + +- Куда открывать карточку: активная панель, всплывающее окно, новая вкладка, сплит вправо / вниз. +- Ширина карточки (px). +- Колонок в группе (1–6), сетка внутри колонки. +- Зебра колонок (чередование фона для сканирования). +- Masonry-раскладка (карточки заполняют вертикальные пустоты). + +**Title** + +- Перенос заголовка, жирный заголовок, размер шрифта (px). + +**Properties** + +- Раскладка чипов: стек (вертикально) или сетка (компактно в строку). +- Перенос многострочных значений, размер шрифта. +- Формат даты и даты-времени + локаль (Moment.js). + +**Cover** + +- Источник: нет / первая картинка в заметке / свойство-обложка. +- Стиль (без рамки / с рамкой), соотношение сторон, ориентация, вписывание, позиция на карточке. + +> **Callout (💡):** Сплит-превью сделал как в самом Obsidian: левый клик переиспользует панель, Ctrl+Клик, новую вкладку внутри сплита. Без этого на широком экране карточки неудобно просматривать рядом с доской. + +### Работает внутри штатного тулбара + +Вид цепляется к BasesView-фреймворку, поэтому сортировка, фильтр, свойства и поиск из тулбара Bases остаются рабочими. Плагин только рисует результат запроса, данные не дублируются и не зависят от отдельного хранилища. + +## Результат + +Вид закрыл мою боль: Bases-запрос теперь читается как доска, а не как плоская лента карточек. Группировка по свойству + отбор по тегам И/ИЛИ, то, чего не хватало штатному виду. Всё крутится на запросе Bases, так что перенос vault-а и синхронизация ничего не ломают. + +Метрики у пет-проекта нет, и придумывать её не стану. Проверяю проще: юзабилити на себе в ежедневной работе. Критерий, сколько действий нужно, чтобы отобрать заметки по двум тегам и найти нужную карточку. В штатном виде карточек это был скролл ленты с ручным поиском по заголовкам; здесь, два клика по пилюлям и колонка перед глазами. Если бы замерял всерьёз, считал бы время на задачу на выборке из 20–30 заметок до и после, но на своей повседневной базе разница заметна и без секундомера. + +Что сделал бы иначе. Drag-and-drop карточек между колонками не стал делать сознательно, это меняет свойство в заметке, и для view-плагина это отдельный риск целостности данных. Но вижу запрос на это у пользователей, и, возможно, добавлю как опциональный режим. А пока плагин живёт вручную (через community-каталог ещё не прошёл), это единственное, что мешает им пользоваться сходу. + +> **Callout (✅):** Главный вывод: вид для Bases получается рабочим только тогда, когда он уважает тулбар фреймворка. Я добавил представление, а не вторую систему хранения, и заметки остались обычными заметками. + +## Медиа-плейсхолдеры (в HTML закомментированы) + +| Слот | Файл | Что показывать | +|---|---|---| +| Обзор | `overview.webp` | Вид Card Columns поверх `.base` файла | +| Колонки | `board.webp` | Колонки карточек с группировкой по свойству | +| Фильтр | `filter.webm` / `filter.mp4` + `filter-poster.webp` | Фильтрация по тегам, OR и AND | +| Настройки | `settings.webp` | Gear-меню с секциями | + +Ещё один слот (`note-before.webp`) остался в шаблоне из ранней версии и сейчас не используется, удалить при вставке медиа. + +## Источники фактуры + +- Репозиторий: `https://github.com/mamadaevv/card-columns` (лицензия MIT). +- Все параметры кастомизации и логика фильтрации взяты из README репозитория. +- Drag-and-drop в плагине **отсутствует**: он есть у соседнего `ewerx/obsidian-bases-kanban`, не путать. diff --git a/two.mjs b/two.mjs new file mode 100644 index 0000000..a70dab6 --- /dev/null +++ b/two.mjs @@ -0,0 +1,14 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const top = await p.evaluate(()=>{ const r=document.querySelector('[data-shader-demo]').getBoundingClientRect(); return r.top+window.scrollY; }); +await p.evaluate((t)=>window.scrollTo(0, t-150), top); +await new Promise(r=>setTimeout(r,700)); +await p.screenshot({path:'/tmp/t1.png'}); +await new Promise(r=>setTimeout(r,2500)); +await p.screenshot({path:'/tmp/t2.png'}); +console.log('done'); +await b.close(); diff --git a/twoframes.mjs b/twoframes.mjs new file mode 100644 index 0000000..d6fc21b --- /dev/null +++ b/twoframes.mjs @@ -0,0 +1,21 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,800)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + // два render подряд, потому что drawArrays инициализируется лениво на 1-м вызове + m.render(performance.now()); m.render(performance.now()); + const px=new Uint8Array(4); gl.readPixels(Math.floor(m.canvasElement.width/2), Math.floor(m.canvasElement.height/2),1,1,gl.RGBA,gl.UNSIGNED_BYTE,px); + // и с разным временем + m.setFrame(0); m.render(performance.now()); m.render(performance.now()); + const px0=new Uint8Array(4); gl.readPixels(50,50,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px0); + m.setFrame(10000); m.render(performance.now()); m.render(performance.now()); + const px1=new Uint8Array(4); gl.readPixels(50,50,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px1); + return { centerAfter2: Array.from(px), at0: Array.from(px0), at10000: Array.from(px1) }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/ucol.mjs b/ucol.mjs new file mode 100644 index 0000000..05414b5 --- /dev/null +++ b/ucol.mjs @@ -0,0 +1,20 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + const out=[]; + for(let i=0;i<4;i++){ + const loc=gl.getUniformLocation(m.program,'u_colors['+i+']'); + const v=new Float32Array(4); + gl.getUniform(gl,m.program,loc,v); // WebGL2? попробуем + out.push(Array.from(v)); + } + return { uColors: out, colorsCount: (()=>{const l=gl.getUniformLocation(m.program,'u_colorsCount');const v=new Int32Array(1);try{gl.getUniform(gl,m.program,l,v);}catch(e){return 'err';}return v[0];})() }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/uloc.mjs b/uloc.mjs new file mode 100644 index 0000000..bc49e49 --- /dev/null +++ b/uloc.mjs @@ -0,0 +1,22 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1000)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + // u_time location + const tloc = gl.getUniformLocation(m.program, 'u_time'); + // прочитаем текущее значение u_time (если location валиден) + let tval=null; + try { const buf=new Float32Array(1); gl.getUniform(gl, m.program, tloc); } catch(e){} + // применим setFrame и проверим, что вообще рисуется (readPixels центр) + m.setFrame(500); + const px=new Uint8Array(4); + gl.readPixels(Math.floor(m.canvasElement.width/2), Math.floor(m.canvasElement.height/2), 1,1, gl.RGBA, gl.UNSIGNED_BYTE, px); + return { hasProgram: !!m.program, uTimeLocIsNull: tloc===null, centerPx: Array.from(px) }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/utime.mjs b/utime.mjs new file mode 100644 index 0000000..20467cf --- /dev/null +++ b/utime.mjs @@ -0,0 +1,19 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + const uloc=gl.getUniformLocation(m.program,'u_time'); + const read=(x,y)=>{const px=new Uint8Array(4);gl.readPixels(x,y,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px);return Array.from(px);}; + const draw=()=>{gl.useProgram(m.program);gl.drawArrays(gl.TRIANGLES,0,6);}; + gl.uniform1f(uloc,0); draw(); const a=read(360,220); + gl.uniform1f(uloc,50); draw(); const c=read(360,220); + gl.uniform1f(uloc,200); draw(); const d=read(360,220); + return { t0:a, t50:c, t200:d }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/verify.mjs b/verify.mjs new file mode 100644 index 0000000..2ab838b --- /dev/null +++ b/verify.mjs @@ -0,0 +1,23 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + gl.getError(); + m.render(performance.now()); m.render(performance.now()); // прогрев drawArrays + const px=new Uint8Array(4); gl.readPixels(360,220,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px); + const a=m.canvasElement.toDataURL('image/png'); + m.setFrame(0); m.render(performance.now()); m.render(performance.now()); + const p0=new Uint8Array(4); gl.readPixels(40,40,1,1,gl.RGBA,gl.UNSIGNED_BYTE,p0); + const u0=m.canvasElement.toDataURL('image/png'); + m.setFrame(10000); m.render(performance.now()); m.render(performance.now()); + const p1=new Uint8Array(4); gl.readPixels(40,40,1,1,gl.RGBA,gl.UNSIGNED_BYTE,p1); + const u1=m.canvasElement.toDataURL('image/png'); + return { centerOpaque: Array.from(px), at0: Array.from(p0), at10000: Array.from(p1), dataURLchanged: u0!==u1 }; +}); +console.log(JSON.stringify(r,null,1)); +await b.close(); diff --git a/vp.mjs b/vp.mjs new file mode 100644 index 0000000..402c6de --- /dev/null +++ b/vp.mjs @@ -0,0 +1,21 @@ +import puppeteer from 'puppeteer'; +const b = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox','--use-gl=angle','--enable-unsafe-swiftshader','--ignore-gpu-blocklist'] }); +const p = await b.newPage(); +await p.setViewport({width:1100,height:1300}); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1200)); +const r = await p.evaluate(()=>{ + const m=document.querySelector('[data-shader-demo] .card-shader').paperShaderMount; + const gl=m.gl; + const before={ w:m.canvasElement.width, h:m.canvasElement.height, vp:[gl.getParameter(gl.VIEWPORT)[0],gl.getParameter(gl.VIEWPORT)[1],gl.getParameter(gl.VIEWPORT)[2],gl.getParameter(gl.VIEWPORT)[3]] }; + // принудительно resize + render + m.handleResize?.(); + m.render(performance.now()); m.render(performance.now()); + const px=new Uint8Array(4); gl.readPixels(360,220,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px); + // макс по canvas + const W=m.canvasElement.width,H=m.canvasElement.height; const buf=new Uint8Array(W*H*4); gl.readPixels(0,0,W,H,gl.RGBA,gl.UNSIGNED_BYTE,buf); + let maxc=0; for(let i=0;i{ if(['error','warning'].includes(m.type())) errs.push(m.type()+': '+m.text()); }); +p.on('pageerror',e=>errs.push('PAGEERR: '+e.message)); +await p.goto('http://10.200.201.3:4175/_lab/neu-card.html',{waitUntil:'load',timeout:60000}); +await new Promise(r=>setTimeout(r,1500)); +const r = await p.evaluate(()=>{ + const wrap=document.querySelector('[data-shader-demo] .card-shader'); + const card=document.querySelector('[data-shader-demo]'); + const cv=wrap?wrap.querySelector('canvas'):null; + // можно ли создать webgl вручную? + const test=document.createElement('canvas'); test.width=10; test.height=10; + const gl=test.getContext('webgl')||test.getContext('webgl2'); + return { + wrapW: wrap?Math.round(wrap.getBoundingClientRect().width):0, + wrapH: wrap?Math.round(wrap.getBoundingClientRect().height):0, + cardW: card?Math.round(card.getBoundingClientRect().width):0, + cardH: card?Math.round(card.getBoundingClientRect().height):0, + canvasExists: !!cv, + canvasW: cv?cv.width:0, canvasH: cv?cv.height:0, + canvasCssW: cv?Math.round(cv.getBoundingClientRect().width):0, + webglWorks: !!gl, + renderer: gl?gl.getParameter(gl.VERSION):'none', + }; +}); +console.log(JSON.stringify({r, errs}, null, 1)); +await b.close();