diff --git a/src/editor.ts b/src/editor.ts index 9bbcddf..e2e542d 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -539,48 +539,50 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S let compressor: PngCompressor; events.function('scene.saveScreenshot', async () => { - const texture = scene.camera.entity.camera.renderTarget.colorBuffer; - await texture.downloadAsync(); - - // construct the png compressor - if (!compressor) { - compressor = new PngCompressor(); - } - - // @ts-ignore - const pixels = new Uint8ClampedArray(texture.getSource().buffer.slice()); - - // the render buffer contains premultiplied alpha. so apply background color. - const bkClr = 102; - for (let i = 0; i < pixels.length; i += 4) { - const r = pixels[i]; - const g = pixels[i + 1]; - const b = pixels[i + 2]; - const a = pixels[i + 3]; - - pixels[i + 0] = r + (255 - a) * bkClr / 255; - pixels[i + 1] = g + (255 - a) * bkClr / 255; - pixels[i + 2] = b + (255 - a) * bkClr / 255; - pixels[i + 3] = 255; - } + events.fire('startSpinner'); + + try { + const texture = scene.camera.entity.camera.renderTarget.colorBuffer; + await texture.downloadAsync(); + + // construct the png compressor + if (!compressor) { + compressor = new PngCompressor(); + } - const arrayBuffer = await compressor.compress( - new Uint32Array(pixels.buffer), - texture.width, - texture.height - ); - - // construct filename - const filename = replaceExtension(selectedSplats()?.[0]?.filename ?? 'SuperSplat', '.png'); - - // download - const blob = new Blob([arrayBuffer], { type: 'octet/stream' }); - const url = window.URL.createObjectURL(blob); - const el = document.createElement('a'); - el.download = filename; - el.href = url; - el.click(); - window.URL.revokeObjectURL(url); + // @ts-ignore + const pixels = new Uint8ClampedArray(texture.getSource().buffer.slice()); + + // the render buffer contains premultiplied alpha. so apply background color. + const { r, g, b } = events.invoke('bgClr'); + for (let i = 0; i < pixels.length; i += 4) { + const a = 255 - pixels[i + 3]; + pixels[i + 0] += r * a; + pixels[i + 1] += g * a; + pixels[i + 2] += b * a; + pixels[i + 3] = 255; + } + + const arrayBuffer = await compressor.compress( + new Uint32Array(pixels.buffer), + texture.width, + texture.height + ); + + // construct filename + const filename = replaceExtension(selectedSplats()?.[0]?.filename ?? 'SuperSplat', '.png'); + + // download + const blob = new Blob([arrayBuffer], { type: 'octet/stream' }); + const url = window.URL.createObjectURL(blob); + const el = document.createElement('a'); + el.download = filename; + el.href = url; + el.click(); + window.URL.revokeObjectURL(url); + } finally { + events.fire('stopSpinner'); + } }); } diff --git a/src/main.ts b/src/main.ts index 238221b..b092f2d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -136,10 +136,31 @@ const main = async () => { graphicsDevice ); - // configure body background color - const clr = sceneConfig.bgClr; - const cnv = (v: number) => `${Math.max(0, Math.min(255, (v * 255))).toFixed(0)}` - document.body.style.backgroundColor = `rgba(${cnv(clr.r)},${cnv(clr.g)},${cnv(clr.b)},${clr.a.toFixed(2)})`; + // background color + const clr = { r: -1, g: -1, b: -1 }; + + const setBgClr = (r: number, g: number, b: number) => { + if (r !== clr.r || g !== clr.g || b !== clr.b) { + clr.r = r; + clr.g = g; + clr.b = b; + + const cnv = (v: number) => `${Math.max(0, Math.min(255, (v * 255))).toFixed(0)}` + document.body.style.backgroundColor = `rgba(${cnv(r)},${cnv(g)},${cnv(b)},1)`; + + events.fire('bgClr', r, g, b); + } + }; + + events.on('setBgClr', (r: number, g: number, b: number) => { + setBgClr(r, g, b); + }); + + events.function('bgClr', () => { + return { r: clr.r, g: clr.g, b: clr.b }; + }); + + setBgClr(sceneConfig.bgClr.r, sceneConfig.bgClr.g, sceneConfig.bgClr.b); // tool manager const toolManager = new ToolManager(events); diff --git a/src/ui/localization.ts b/src/ui/localization.ts index e9e4111..ef18423 100644 --- a/src/ui/localization.ts +++ b/src/ui/localization.ts @@ -59,6 +59,7 @@ const localizeInit = () => { // Options panel "options": "ANSICHTS OPTIONEN", + "options.bg-clr": "Hintergrundfarbe", "options.fov": "Blickwinkel (FoV)", "options.sh-bands": "SH Bänder", "options.centers-size": "Punktgrößen", @@ -201,6 +202,7 @@ const localizeInit = () => { // Options panel "options": "VIEW OPTIONS", + "options.bg-clr": "Background Color", "options.fov": "Field of View", "options.sh-bands": "SH Bands", "options.centers-size": "Centers Size", @@ -343,6 +345,7 @@ const localizeInit = () => { // options panel "options": "OPTIONS D'AFFICHAGE", + "options.bg-clr": "Couleur de fond", "options.fov": "Champ de vision", "options.sh-bands": "Ordres d'HS", "options.centers-size": "Échelle des centres", @@ -478,6 +481,7 @@ const localizeInit = () => { // Options panel "options": "表示オプション", + "options.bg-clr": "背景色", "options.fov": "視野 ( FOV )", "options.sh-bands": "球面調和関数のバンド", "options.centers-size": "センターサイズ", @@ -619,6 +623,7 @@ const localizeInit = () => { // Options panel "options": "보기 옵션", + "options.bg-clr": "배경 색상", "options.fov": "시야각", "options.sh-bands": "SH 밴드", "options.centers-size": "센터 크기", @@ -760,6 +765,7 @@ const localizeInit = () => { // Options panel "options": "视图选项", + "options.bg-clr": "背景颜色", "options.fov": "视野角", "options.sh-bands": "SH 带", "options.centers-size": "中心大小", diff --git a/src/ui/view-panel.scss b/src/ui/view-panel.scss index 8cf8ded..f5e8e52 100644 --- a/src/ui/view-panel.scss +++ b/src/ui/view-panel.scss @@ -3,11 +3,11 @@ transform: translate(0, -50%); right: 102px; width: 320px; + flex-direction: column; - :not(.pcui-hidden) { + &:not(.pcui-hidden) { display: flex; } - flex-direction: column; & > .view-panel-row { display: flex; @@ -47,6 +47,11 @@ } } } + + &>.view-panel-row-picker { + margin: 2px 6px; + height: 24px; + } } > .view-panel-list-container { diff --git a/src/ui/view-panel.ts b/src/ui/view-panel.ts index afbd653..6b43be9 100644 --- a/src/ui/view-panel.ts +++ b/src/ui/view-panel.ts @@ -1,5 +1,5 @@ import { Vec3 } from 'playcanvas'; -import { BooleanInput, Container, Label, SliderInput } from 'pcui'; +import { BooleanInput, ColorPicker, Container, Label, SliderInput } from 'pcui'; import { Events } from '../events'; import { Tooltips } from './tooltips'; import { localize } from './localization'; @@ -39,6 +39,25 @@ class ViewPanel extends Container { header.append(icon); header.append(label); + // background color + + const bgClrRow = new Container({ + class: 'view-panel-row' + }); + + const bgClrLabel = new Label({ + text: localize('options.bg-clr'), + class: 'view-panel-row-label' + }); + + const bgClrPicker = new ColorPicker({ + class: 'view-panel-row-picker', + value: [0.4, 0.4, 0.4] + }); + + bgClrRow.append(bgClrLabel); + bgClrRow.append(bgClrPicker); + // camera fov const fovRow = new Container({ @@ -226,6 +245,7 @@ class ViewPanel extends Container { poseListContainer.append(poseList); this.append(header); + this.append(bgClrRow); this.append(fovRow); this.append(shBandsRow); this.append(centersSizeRow); @@ -446,6 +466,12 @@ class ViewPanel extends Container { } }); + // background color + + bgClrPicker.on('change', (value: number[]) => { + events.fire('setBgClr', value[0], value[1], value[2]); + }); + // camera fov events.on('camera.fov', (fov: number) => {