forked from natuka/web.puabadge.com
修改:优化手机端效果图点云效果
This commit is contained in:
1 parent
605bd894c5
commit
5892c47a6b
5 files changed
+132
-43
No files matched your search
+5
-1
@@ -4,7 +4,11 @@ const PREFIX = 'douyin'
|
||||
|
||||
function normalizeOssUrl(url: string) {
|
||||
if (!url) return ''
|
||||
return String(url).replace(/\\u0026/gi, '&').replace(/\\u003d/gi, '=')
|
||||
let s = String(url).replace(/\\u0026/gi, '&').replace(/\\u003d/gi, '=').trim()
|
||||
// 真机 https 页加载 http 图会被拦截,统一升 https
|
||||
if (s.indexOf('//') === 0) s = `https:${s}`
|
||||
if (/^http:\/\//i.test(s)) s = `https://${s.slice(7)}`
|
||||
return s
|
||||
}
|
||||
|
||||
function guessContentType(file: File | Blob, filename?: string) {
|
||||
|
||||
@@ -186,9 +186,9 @@ function preloadResult(url: string) {
|
||||
if (!url || !renderer || !sm) return
|
||||
resultKey = url
|
||||
sm.setResultPreloadStatus('loading')
|
||||
// 效果图保留完整画面做点云(去背景会在手机上采空,表现为「没有效果图点云」)
|
||||
// 效果图点云去掉背景,只保留主体
|
||||
renderer.textures
|
||||
.load(url, { removeBackground: false })
|
||||
.load(url, { removeBackground: true })
|
||||
.then((entry: any) => {
|
||||
if (destroyed || !sm) return
|
||||
if (!entry || !entry.pointCloudBuffer) {
|
||||
@@ -428,14 +428,16 @@ onUnmounted(() => {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 仅占位;真实采样已改用离屏 canvas,勿再用 CSS 锁尺寸 */
|
||||
.ipc-scale {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
left: -9999px !important;
|
||||
top: 0 !important;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
left: -9999px !important;
|
||||
top: auto !important;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.ipc-fallback {
|
||||
|
||||
@@ -46,11 +46,11 @@ function createPointCloudSampler(scaleCanvas, viewportW, viewportH, sharedCtx) {
|
||||
const forceSampleSize = options.forceSampleSize || null
|
||||
|
||||
const image = entry.image
|
||||
if (!image || !image.width || !image.height) {
|
||||
const srcW = (image && (image.naturalWidth || image.width)) || 0
|
||||
const srcH = (image && (image.naturalHeight || image.height)) || 0
|
||||
if (!image || !srcW || !srcH) {
|
||||
throw new Error('image not ready for sampling')
|
||||
}
|
||||
const srcW = image.width
|
||||
const srcH = image.height
|
||||
let w
|
||||
let h
|
||||
if (forceSampleSize && forceSampleSize.width && forceSampleSize.height) {
|
||||
@@ -152,13 +152,20 @@ function createPointCloudSampler(scaleCanvas, viewportW, viewportH, sharedCtx) {
|
||||
const dg = g - bgG
|
||||
const db = b - bgB
|
||||
const distance = Math.sqrt(dr * dr + dg * dg + db * db) / 441.67
|
||||
const notBackground = Math.max(0, Math.min(1, (distance - 0.025) / 0.09))
|
||||
const softKnockout = !!options.softKnockout
|
||||
// 默认更狠地抠掉接近四角背景色的点;softKnockout 略放宽防采空
|
||||
const distCut = softKnockout ? 0.028 : 0.05
|
||||
const distRamp = softKnockout ? 0.08 : 0.07
|
||||
if (distance < distCut) continue
|
||||
const notBackground = Math.max(0, Math.min(1, (distance - distCut) / distRamp))
|
||||
const chroma = Math.abs(r - g) + Math.abs(g - b) + Math.abs(r - b)
|
||||
const nearBlack = lum < 0.07 && chroma < 16
|
||||
if (nearBlack) continue
|
||||
const darkKeep = Math.max(0, Math.min(1, (0.62 - lum) / 0.45))
|
||||
const softKeep = Math.max(0, Math.min(1, (distance - 0.015) / 0.07))
|
||||
presence = Math.max(notBackground, darkKeep * 0.5, softKeep * 0.4) * a
|
||||
const bgLum = luminance(bgR, bgG, bgB)
|
||||
// 仅亮底时丢弃近黑噪点,避免黑底效果图误杀主体暗部
|
||||
if (bgLum > 0.35) {
|
||||
const nearBlack = lum < 0.07 && chroma < 16
|
||||
if (nearBlack) continue
|
||||
}
|
||||
presence = notBackground * a
|
||||
subjectWeight = 1
|
||||
}
|
||||
|
||||
|
||||
@@ -235,11 +235,41 @@ function createRenderer(canvas, cssW, cssH, dpr, scaleCanvas) {
|
||||
gl.clear(gl.COLOR_BUFFER_BIT)
|
||||
|
||||
const pointOpacity = u.opacity != null ? u.opacity : 1
|
||||
const reveal = u.reveal != null ? u.reveal : 0
|
||||
|
||||
// 完整效果图改由页面层按预览框位展示,避免 WebGL 全屏图与预览错位
|
||||
if (pointOpacity > 0.001 && cloudA && cloudB) {
|
||||
gl.enable(gl.BLEND)
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
gl.enable(gl.BLEND)
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
|
||||
// 揭示阶段绘制效果图纹理,避免点云淡出后整页黑屏
|
||||
if (hasResult && reveal > 0.001 && resultTex) {
|
||||
const fit = containFit(contentAspect || 1, cssW, cssH, 0.98)
|
||||
gl.useProgram(quadProgram)
|
||||
gl.activeTexture(gl.TEXTURE0)
|
||||
gl.bindTexture(gl.TEXTURE_2D, resultTex)
|
||||
gl.uniform1i(quadUni.uTexture, 0)
|
||||
gl.uniform1f(quadUni.uOpacity, Math.min(1, reveal))
|
||||
gl.uniform2f(quadUni.uContentScale, fit.scaleX, fit.scaleY)
|
||||
gl.uniform2f(quadUni.uContentOffset, fit.offsetX, fit.offsetY)
|
||||
gl.uniform4f(
|
||||
quadUni.uBgColor,
|
||||
VISUAL.STAGE_BG[0],
|
||||
VISUAL.STAGE_BG[1],
|
||||
VISUAL.STAGE_BG[2],
|
||||
1
|
||||
)
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer)
|
||||
if (quadAttr.aPosition >= 0) {
|
||||
gl.enableVertexAttribArray(quadAttr.aPosition)
|
||||
gl.vertexAttribPointer(quadAttr.aPosition, 2, gl.FLOAT, false, 16, 0)
|
||||
}
|
||||
if (quadAttr.aUv >= 0) {
|
||||
gl.enableVertexAttribArray(quadAttr.aUv)
|
||||
gl.vertexAttribPointer(quadAttr.aUv, 2, gl.FLOAT, false, 16, 8)
|
||||
}
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
||||
}
|
||||
|
||||
if (pointOpacity > 0.001 && cloudA && cloudB && cloudA.buffer && cloudB.buffer) {
|
||||
gl.useProgram(pointProgram)
|
||||
|
||||
bindCloudAttribs(
|
||||
|
||||
@@ -14,23 +14,27 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) {
|
||||
let maxTexSize = VISUAL.MAX_TEX_SIZE
|
||||
let maxPoints = POINT_CLOUD.MEDIUM
|
||||
let sampleSide = POINT_CLOUD.SAMPLE_MEDIUM
|
||||
let scaleCtx = null
|
||||
|
||||
if (scaleCanvas) {
|
||||
/**
|
||||
* 真机关键:不要用 DOM 里被 CSS 锁成 1×1 的 canvas 做 getImageData。
|
||||
* 部分 WebView 会把绘图缓冲钳到 CSS 尺寸,导致效果图采点为空 → 整页黑屏。
|
||||
*/
|
||||
const sampleCanvas = document.createElement('canvas')
|
||||
let scaleCtx = null
|
||||
try {
|
||||
scaleCtx = sampleCanvas.getContext('2d', { willReadFrequently: true })
|
||||
} catch (e) {
|
||||
try {
|
||||
// getImageData 频繁读回:声明 willReadFrequently 消除告警并提升性能
|
||||
scaleCtx = scaleCanvas.getContext('2d', { willReadFrequently: true })
|
||||
} catch (e) {
|
||||
try {
|
||||
scaleCtx = scaleCanvas.getContext('2d')
|
||||
} catch (e2) {
|
||||
scaleCtx = null
|
||||
}
|
||||
scaleCtx = sampleCanvas.getContext('2d')
|
||||
} catch (e2) {
|
||||
scaleCtx = null
|
||||
}
|
||||
}
|
||||
// 保留入参 scaleCanvas 作兼容,实际采样/缩放一律走离屏 canvas
|
||||
void scaleCanvas
|
||||
|
||||
const sampler = createPointCloudSampler(
|
||||
scaleCanvas,
|
||||
sampleCanvas,
|
||||
viewportW,
|
||||
viewportH,
|
||||
scaleCtx
|
||||
@@ -114,11 +118,20 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeImageSrc(src) {
|
||||
let s = String(src || '').trim()
|
||||
if (!s) return ''
|
||||
if (s.indexOf('//') === 0) s = 'https:' + s
|
||||
if (/^http:\/\//i.test(s)) s = 'https://' + s.slice(7)
|
||||
return s
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载图片并严格在 onload 之后再交给后续逻辑。
|
||||
* http(s) 优先 fetch→blob,避免手机 WebView 跨域导致 getImageData 采点失败。
|
||||
*/
|
||||
function loadImage(src) {
|
||||
function loadImage(rawSrc) {
|
||||
const src = normalizeImageSrc(rawSrc)
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (!src) {
|
||||
reject(new Error('empty src'))
|
||||
@@ -136,8 +149,8 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) {
|
||||
if (settled) return
|
||||
setTimeout(function () {
|
||||
if (settled) return
|
||||
const w = img.width || 0
|
||||
const h = img.height || 0
|
||||
const w = img.naturalWidth || img.width || 0
|
||||
const h = img.naturalHeight || img.height || 0
|
||||
if (!w || !h) {
|
||||
settled = true
|
||||
if (revokeUrl) {
|
||||
@@ -173,15 +186,36 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) {
|
||||
|
||||
// 远程图:先 blob 同源化,再采样(手机端效果图点云依赖此路径)
|
||||
if (/^https?:\/\//i.test(src) && typeof fetch === 'function') {
|
||||
fetch(src, { mode: 'cors', credentials: 'omit', cache: 'force-cache' })
|
||||
fetch(src, { mode: 'cors', credentials: 'omit', cache: 'no-cache' })
|
||||
.then(function (res) {
|
||||
if (!res || !res.ok) throw new Error('fetch image http ' + (res && res.status))
|
||||
return res.blob()
|
||||
})
|
||||
.then(function (blob) {
|
||||
if (!blob || !blob.size) throw new Error('empty image blob')
|
||||
// createImageBitmap 在部分 WebView 上更稳,再回退 blob URL
|
||||
if (typeof createImageBitmap === 'function') {
|
||||
return createImageBitmap(blob).then(function (bitmap) {
|
||||
if (bitmap && bitmap.width && bitmap.height) {
|
||||
resolve({
|
||||
image: bitmap,
|
||||
path: src,
|
||||
width: bitmap.width,
|
||||
height: bitmap.height,
|
||||
revokeUrl: '',
|
||||
})
|
||||
return null
|
||||
}
|
||||
throw new Error('empty bitmap')
|
||||
}).catch(function () {
|
||||
const objUrl = URL.createObjectURL(blob)
|
||||
bindImage(objUrl, objUrl)
|
||||
return null
|
||||
})
|
||||
}
|
||||
const objUrl = URL.createObjectURL(blob)
|
||||
bindImage(objUrl, objUrl)
|
||||
return null
|
||||
})
|
||||
.catch(function () {
|
||||
// CORS 拉流失败时退回 <img crossOrigin>
|
||||
@@ -195,11 +229,11 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) {
|
||||
}
|
||||
|
||||
function uploadTexture(image, path) {
|
||||
if (!image || !image.width || !image.height) {
|
||||
if (!image || !(image.width || image.naturalWidth) || !(image.height || image.naturalHeight)) {
|
||||
throw new Error('uploadTexture: image not ready')
|
||||
}
|
||||
const srcW = image.width || 1
|
||||
const srcH = image.height || 1
|
||||
const srcW = image.naturalWidth || image.width || 1
|
||||
const srcH = image.naturalHeight || image.height || 1
|
||||
let drawW = srcW
|
||||
let drawH = srcH
|
||||
const longest = Math.max(srcW, srcH)
|
||||
@@ -212,9 +246,9 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) {
|
||||
const tex = createEmptyTexture()
|
||||
gl.bindTexture(gl.TEXTURE_2D, tex)
|
||||
try {
|
||||
if ((drawW !== srcW || drawH !== srcH) && scaleCanvas && scaleCtx) {
|
||||
scaleCanvas.width = drawW
|
||||
scaleCanvas.height = drawH
|
||||
if ((drawW !== srcW || drawH !== srcH) && sampleCanvas && scaleCtx) {
|
||||
sampleCanvas.width = drawW
|
||||
sampleCanvas.height = drawH
|
||||
scaleCtx.clearRect(0, 0, drawW, drawH)
|
||||
scaleCtx.drawImage(image, 0, 0, drawW, drawH)
|
||||
gl.texImage2D(
|
||||
@@ -223,7 +257,7 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) {
|
||||
gl.RGBA,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
scaleCanvas
|
||||
sampleCanvas
|
||||
)
|
||||
} else {
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)
|
||||
@@ -331,11 +365,23 @@ function createTextureManager(gl, canvas, scaleCanvas, viewportW, viewportH) {
|
||||
entry.height = uploaded.height
|
||||
entry.aspect = uploaded.aspect
|
||||
|
||||
const cloud = sampler.sample(entry, {
|
||||
let cloud = sampler.sample(entry, {
|
||||
maxPoints: maxPoints,
|
||||
maxSide: sampleSide,
|
||||
removeBackground: removeBackground,
|
||||
})
|
||||
// 采点过稀:降分辨率再采,仍保持去背景(效果图不要背景点云)
|
||||
if (!cloud || cloud.count < Math.max(40, maxPoints * 0.01)) {
|
||||
cloud = sampler.sample(entry, {
|
||||
maxPoints: maxPoints,
|
||||
maxSide: Math.min(sampleSide, POINT_CLOUD.SAMPLE_LOW),
|
||||
removeBackground: removeBackground,
|
||||
softKnockout: true,
|
||||
})
|
||||
}
|
||||
if (!cloud || cloud.count < Math.max(80, Math.floor(maxPoints * 0.02))) {
|
||||
throw new Error('point cloud sample too sparse: ' + (cloud && cloud.count))
|
||||
}
|
||||
entry.pointCloud = cloud
|
||||
entry.pointCloudBuffer = uploadCloudBuffer(cloud)
|
||||
entry.preloadStatus = 'ready'
|
||||
|
||||
Reference in new issue
Block a user