Просмотр исходного кода

Improve CanvasSnapshotPlugin with more options, Add promptForUrl button, load function in DropzonePlugin, Add initialized event in GLTFMeshOptDecodePlugin.

master
Palash Bansal 1 год назад
Родитель
Сommit
9fded61e2e
Аккаунт пользователя с таким Email не найден

+ 23
- 2
examples/canvas-snapshot-plugin/script.ts Просмотреть файл

@@ -1,11 +1,18 @@
import {_testFinish, CanvasSnapshotPlugin, isWebpExportSupported, LoadingScreenPlugin, ThreeViewer} from 'threepipe'
import {
_testFinish,
CanvasSnapshotPlugin,
isWebpExportSupported,
LoadingScreenPlugin,
SSAAPlugin,
ThreeViewer,
} from 'threepipe'
import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js'

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
renderScale: 'auto',
plugins: [LoadingScreenPlugin],
plugins: [LoadingScreenPlugin, SSAAPlugin],
})

async function init() {
@@ -24,6 +31,8 @@ async function init() {
mimeType: 'image/jpeg', // mime type of the image
quality: 0.9, // quality of the image (0-1) only for jpeg and webp
displayPixelRatio: 2, // render scale
waitForProgressive: true,
progressiveFrames: 64, // wait for 64 frames of ProgressivePlugin/SSAA before exporting
})
btn.disabled = false
},
@@ -73,6 +82,18 @@ async function init() {
})
btn.disabled = false
},
['Download 3x3 Tiles (png zip)']: async(btn: HTMLButtonElement) => {
btn.disabled = true
await snapshotPlugin.downloadSnapshot('snapshot', {
mimeType: 'image/png', // mime type of the image
// scale: 1, // scale the final image
// quality: 0.9, // quality of the image (0-1) only for jpeg and webp
displayPixelRatio: 4, // render scale
tileRows: 3,
tileColumns: 3,
})
btn.disabled = false
},
})

}

+ 147
- 33
src/plugins/export/CanvasSnapshotPlugin.ts Просмотреть файл

@@ -1,8 +1,37 @@
import {serialize, timeout} from 'ts-browser-helpers'
import {AViewerPluginSync} from '../../viewer'
import {uiButton, uiConfig, uiFolderContainer, uiInput} from 'uiconfig.js'
import {uiButton, uiConfig, uiFolderContainer, uiInput, uiVector} from 'uiconfig.js'
import {CanvasSnapshot, CanvasSnapshotOptions} from '../../utils/canvas-snapshot'
import {ProgressivePlugin} from '../pipeline/ProgressivePlugin'
import {Zippable, zipSync} from 'three/examples/jsm/libs/fflate.module.js'
import {Vector4} from 'three'

export interface CanvasSnapshotPluginOptions extends CanvasSnapshotOptions{
/**
* If true, will wait for progressive rendering(requires {@link ProgressivePlugin}) to finish before taking snapshot
* @default true
*/
waitForProgressive?: boolean
/**
* Number of progressive frames to wait for before taking snapshot
@default 64 or {@link ProgressivePlugin.maxFrameCount}, whichever is higher
*/
progressiveFrames?: number
/**
* Time in ms to wait before taking the snapshot.
* This timeout is applied before `waitForProgressive` if both are specified.
*/
timeout?: number,
/**
* Number of tile rows to split the image into
* @default 1
*/
tileRows?: number
/**
* Number of tile columns to split the image into
*/
tileColumns?: number
}

@uiFolderContainer('Canvas Snapshot (Image Export)')
export class CanvasSnapshotPlugin extends AViewerPluginSync {
@@ -19,7 +48,7 @@ export class CanvasSnapshotPlugin extends AViewerPluginSync {
* @param filename default is {@link CanvasSnapshotPlugin.filename}
* @param options waitForProgressive: wait for progressive rendering to finish, default: true
*/
async getFile(filename?: string, options: CanvasSnapshotOptions&{waitForProgressive?: boolean} = {waitForProgressive: true}): Promise<File|undefined> {
async getFile(filename?: string, options: CanvasSnapshotPluginOptions = {waitForProgressive: true}): Promise<File|undefined> {
return await this._getFile(filename || this.filename, {...options, getDataUrl: false}) as File
}

@@ -27,88 +56,173 @@ export class CanvasSnapshotPlugin extends AViewerPluginSync {
* Returns a data url of the screenshot of the viewer canvas
* @param options waitForProgressive: wait for progressive rendering to finish, default: true
*/
async getDataUrl(options: CanvasSnapshotOptions&{waitForProgressive?: boolean} = {}): Promise<string> {
async getDataUrl(options: CanvasSnapshotPluginOptions = {}): Promise<string> {
return await this._getFile('', {...options, getDataUrl: true}) as string ?? ''
}

private async _getFile(filename: string, options: CanvasSnapshotOptions&{waitForProgressive?: boolean} = {}): Promise<File|string|undefined> {
private async _getFile(filename: string, options: CanvasSnapshotPluginOptions = {}): Promise<File|string|string[]|undefined> {
await this._viewer?.doOnce('postFrame')
const viewer = this._viewer
const canvas = this._viewer?.canvas
if (!viewer || !canvas) return undefined
viewer.scene.mainCamera.setInteractions(false, CanvasSnapshotPlugin.PluginType)

const dpr = viewer.renderManager.renderScale
if (options.displayPixelRatio !== undefined && options.displayPixelRatio !== dpr) {
viewer.renderManager.renderScale = options.displayPixelRatio
}
if (options.timeout) await timeout(options.timeout)

const progressive = viewer.getPlugin(ProgressivePlugin)
if (options.waitForProgressive !== false && progressive) {
// todo: disable interactions and all so that frameCount is not affected
await new Promise<void>((res)=>{
const listener = () => {
if (!progressive.isConverged(true)) return
viewer.removeEventListener('postFrame', listener)
res()
let waitForProgressive = options.waitForProgressive ?? !!progressive
if (waitForProgressive && !progressive) {
viewer.console.warn('CanvasSnapshotPlugin: ProgressivePlugin required to wait for progressive rendering')
waitForProgressive = false
}
if (options.progressiveFrames && !waitForProgressive) {
viewer.console.warn('CanvasSnapshotPlugin: waitForProgressive must be true to use progressiveFrames')
}
const lastMaxFrames = progressive?.maxFrameCount

if (waitForProgressive && progressive) {
progressive.maxFrameCount = Math.max(options.progressiveFrames ?? 64, progressive.maxFrameCount)
viewer.setDirty()
await viewer.doOnce('postFrame')

while (!progressive.isConverged(true)) {
await viewer.doOnce('postFrame')
// console.log(`rendering ${ 100 * this._viewer!.renderer.frameCount / progressive.maxFrameCount }%`)
}
} else {
viewer.setDirty()
await viewer.doOnce('postFrame')
}


delete options.displayPixelRatio
// const rect = options.rect
// if (rect && viewer.renderManager.renderScale !== 1) {
// options.rect = {
// ...rect,
// x: rect.x * viewer.renderManager.renderScale,
// y: rect.y * viewer.renderManager.renderScale,
// width: rect.width * viewer.renderManager.renderScale,
// height: rect.height * viewer.renderManager.renderScale,
// }
// }

let file
if (options.tileRows && options.tileRows > 1 || options.tileColumns && options.tileColumns > 1) {
const res = await CanvasSnapshot.GetTiledFiles(canvas, filename, Math.max(1, options.tileRows || 1), Math.max(1, options.tileColumns || 1), options)
if (Array.isArray(res)) {
if (res.length === 1) file = res[0]
else if (res.length === 0) file = undefined
else if (!options.getDataUrl) {
const zippa: Zippable = {}
for (const f of res) {
zippa[(f as File).name] = new Uint8Array(await (f as File).arrayBuffer())
}
const zipped = zipSync(zippa)
file = new File([zipped], filename + '.zip', {type: 'application/zip', lastModified: Date.now()})
} else {
file = res as string[]
}
viewer.addEventListener('postFrame', listener)
})
} else await viewer.doOnce('postFrame')
options.displayPixelRatio = 1
const rect = options.rect
if (rect && viewer.renderManager.renderScale !== 1) {
options.rect = {
...rect,
x: rect.x * viewer.renderManager.renderScale,
y: rect.y * viewer.renderManager.renderScale,
width: rect.width * viewer.renderManager.renderScale,
height: rect.height * viewer.renderManager.renderScale,
} else {
file = res
}
} else {
file = await CanvasSnapshot.GetFile(canvas, filename, options)
}
const file = await CanvasSnapshot.GetFile(canvas, filename, options)
options.rect = rect
// const file = await CanvasSnapshot.GetFile(canvas, filename, options)

// options.rect = rect
options.displayPixelRatio = viewer.renderManager.renderScale
if (progressive && lastMaxFrames !== undefined) {
progressive.maxFrameCount = lastMaxFrames
}
viewer.scene.mainCamera.setInteractions(true, CanvasSnapshotPlugin.PluginType)
viewer.renderManager.renderScale = dpr

return file
}

@uiInput('Filename')
@serialize()
filename = 'snapshot.png'
filename = 'snapshot'

@uiInput('Frame Count')
@serialize()
progressiveFrames = 64

@uiInput('Tile Rows')
@serialize()
tileRows = 1

@uiInput('Tile Columns')
@serialize()
tileColumns = 1

@uiVector('Crop Rect (x, y, w, h)', [0, 1], 0.001)
@serialize()
rect = new Vector4(0, 0, 1, 1)

private _downloading = false

/**
* Only for {@link downloadSnapshot} and functions using that
*/
@uiConfig()
@serialize()
defaultOptions: CanvasSnapshotOptions&{waitForProgressive?: boolean} = {
defaultOptions: CanvasSnapshotPluginOptions = {
waitForProgressive: true,
displayPixelRatio: window.devicePixelRatio,
scale: 1,
timeout: 0,
quality: 0.9,
tileRows: 1,
tileColumns: 1,
progressiveFrames: 64,
rect: {
x: 0,
y: 0,
width: 1,
height: 1,
normalized: true,
assumeClientRect: false,
},
}

// @uiButton('Download .png', {sendArgs: false})
async downloadSnapshot(filename?: string, options: CanvasSnapshotOptions&{waitForProgressive?: boolean} = {waitForProgressive: true}): Promise<void> {
async downloadSnapshot(filename?: string, options: CanvasSnapshotPluginOptions = {waitForProgressive: true}): Promise<void> {
if (!this._viewer) return
if (!options.mimeType && !filename) this.filename = this.filename.split('.').slice(0, -1).join('.') + '.png'
const file = await this.getFile(filename, {...this.defaultOptions, ...options})
while (this._downloading) {
console.warn('CanvasSnipperPlugin: Another rendering already in progress, waiting...')
await timeout(100)
}
this._downloading = true
// if (!options.mimeType && !filename) this.filename = this.filename.split('.').slice(0, -1).join('.') + '.png'
const file = await this.getFile(filename, {...this.defaultOptions, ...options}).catch(e=>{
this._viewer?.console.error('CanvasSnapshotPlugin: Error exporting file', e)
return null
})

if (file) await this._viewer.exportBlob(file, file.name)
this._downloading = false
}

@uiButton('Download .png')
protected async _downloadPng(): Promise<void> {
this.filename = this.filename.split('.').slice(0, -1).join('.') + '.png'
// this.filename = this.filename.split('.').slice(0, -1).join('.') + '.png'
return this.downloadSnapshot(undefined, {mimeType: 'image/png'})
}
@uiButton('Download .jpeg')
protected async _downloadJpeg(): Promise<void> {
this.filename = this.filename.split('.').slice(0, -1).join('.') + '.jpeg'
// this.filename = this.filename.split('.').slice(0, -1).join('.') + '.jpeg'
return this.downloadSnapshot(undefined, {mimeType: 'image/jpeg'})
}
@uiButton('Download .webp')
protected async _downloadWebp(): Promise<void> {
this.filename = this.filename.split('.').slice(0, -1).join('.') + '.webp'
// this.filename = this.filename.split('.').slice(0, -1).join('.') + '.webp'
return this.downloadSnapshot(undefined, {mimeType: 'image/webp'})
}


+ 3
- 2
src/plugins/import/GLTFMeshOptDecodePlugin.ts Просмотреть файл

@@ -7,7 +7,7 @@ import {SimpleEventDispatcher} from 'ts-browser-helpers'
*
* The plugin name includes GLTF, but its not really GLTF specific, it can be used to decode any meshopt compressed files.
*/
export class GLTFMeshOptDecodePlugin extends SimpleEventDispatcher<''> implements IViewerPluginSync {
export class GLTFMeshOptDecodePlugin extends SimpleEventDispatcher<'initialized'> implements IViewerPluginSync {
declare ['constructor']: typeof GLTFMeshOptDecodePlugin
public static readonly PluginType = 'GLTFMeshOptDecodePlugin'
enabled = true
@@ -48,7 +48,8 @@ window.dispatchEvent(new CustomEvent('${ev}'))
this.rootNode.appendChild(s)
this._script = s
})
return await this._initializing
await this._initializing
this.dispatchEvent({type: 'initialized'})
}

dispose() {

+ 1
- 1
src/plugins/index.ts Просмотреть файл

@@ -48,7 +48,7 @@ export {GLTFMeshOptDecodePlugin} from './import/GLTFMeshOptDecodePlugin'

// export
export {AssetExporterPlugin, type ExportAssetOptions} from './export/AssetExporterPlugin'
export {CanvasSnapshotPlugin, CanvasSnipperPlugin} from './export/CanvasSnapshotPlugin'
export {CanvasSnapshotPlugin, CanvasSnipperPlugin, type CanvasSnapshotPluginOptions} from './export/CanvasSnapshotPlugin'
export {FileTransferPlugin} from './export/FileTransferPlugin'

// postprocessing

+ 43
- 3
src/plugins/interaction/DropzonePlugin.ts Просмотреть файл

@@ -3,8 +3,8 @@ import {AViewerPluginEventMap, type ThreeViewer} from '../../viewer/'
import {AViewerPluginSync} from '../../viewer/AViewerPlugin'
import {Dropzone} from '../../utils'
import {uiButton, uiConfig, uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js'
import type {AddAssetOptions, ImportFilesOptions, ImportResult} from '../../assetmanager'
import {serialize} from 'ts-browser-helpers'
import type {AddAssetOptions, ImportFilesOptions, ImportResult, ImportAddOptions} from '../../assetmanager'
import {parseFileExtension, serialize} from 'ts-browser-helpers'

export interface DropzonePluginOptions {
/**
@@ -112,13 +112,53 @@ export class DropzonePlugin extends AViewerPluginSync<DropzonePluginEventMap> {
/**
* Prompt for file selection using the browser file dialog.
*/
@uiButton('Select files')
@uiButton('Select Local files')
public promptForFile(): void {
if (this.isDisabled()) return
this.allowedExtensions = this._allowedExtensions
this._inputEl?.click()
}

/**
* Prompt for file url.
*/
@uiButton('Import from URL')
public async promptForUrl(): Promise<void> {
if (this.isDisabled() || !this._viewer) return
const res = await this._viewer.dialog.prompt('Enter URL: Enter a public URL for a 3d file with extension', '', true)
if (!res || !res.length) return
await this.load(res, {}, true)
}

async load(res: string, options?: ImportAddOptions, dialog = false) {
if (!this._viewer) {
console.warn('DropzonePlugin: viewer not set')
return
}
if (this.autoImport) {
const manager = this._viewer.assetManager
const ext = parseFileExtension(res)
if (this._allowedExtensions && !this._allowedExtensions.includes(ext)) {
dialog && await this._viewer.dialog.alert(`DropzonePlugin: file extension ${ext} not allowed`)
return
}
const imported = await manager.importer.import(res, {
...this.importOptions,
...options ?? {},
})
const toAdd = [...imported ?? []].flat(2).filter(v => !!v) ?? []
if (this.autoAdd) {
return await manager.loadImported(toAdd, {
...this.addOptions,
...options ?? {},
})
}
return toAdd
} else {
dialog && await this._viewer.dialog.alert('DropzonePlugin: autoImport is disabled, file was not imported')
}
}

private _domElement?: HTMLElement
constructor(options?: DropzonePluginOptions) {
super()

+ 81
- 18
src/utils/canvas-snapshot.ts Просмотреть файл

@@ -7,19 +7,31 @@ export interface CanvasSnapshotRect {
y: number;
/**
* Use if canvas.width !== canvas.clientWidth or height and rect is based on client rect
* @default false
*/
assumeClientRect?: boolean;
/**
* If true, assumes x, y, width, height are normalized to 0-1
* @default false
*/
normalized?: boolean;
}

export interface CanvasSnapshotOptions {
getDataUrl?: boolean,
mimeType?: string,
quality?: number, // between 0 and 1, only for image/jpeg or image/webp
/**
* Crop Region to take snapshot. If not set, the whole canvas is used.
*/
rect?: CanvasSnapshotRect,
scale?: number,
timeout?: number, // in ms, if not specified, will be based on progressive rendering or 200ms
displayPixelRatio?: number,
cloneCanvas?: boolean, // default = true
cloneCanvas?: boolean, // default = true if safari, false otherwise. required for safari where canvas is flipped if premultipliedAlpha is true
}

function isSafari() {
return navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome')
}

export class CanvasSnapshot {
@@ -27,26 +39,39 @@ export class CanvasSnapshot {
public static async GetClonedCanvas(
canvas: HTMLCanvasElement,
{
rect = {x: 0, y: 0, width: canvas.width, height: canvas.height, assumeClientRect: false},
rect = {x: 0, y: 0, width: canvas.width, height: canvas.height, assumeClientRect: false, normalized: false},
displayPixelRatio = 1,
scale = 1,
}: CanvasSnapshotOptions): Promise<HTMLCanvasElement> {
rect = {...rect}

// return canvas.toDataURL(mimeType);
// in Safari, images are flipped when premultipliedAlpha is true in canvas, so it works with 2d context, see: https://github.com/pixijs/pixi.js/blob/dev/packages/extract/src/Extract.ts and https://github.com/pixijs/pixi.js/issues/2951

const destCanvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas') as HTMLCanvasElement
destCanvas.width = rect.width * scale * displayPixelRatio
destCanvas.height = rect.height * scale * displayPixelRatio

// const iRect = {...rect}

if (rect.assumeClientRect) {
rect.x *= canvas.width / (displayPixelRatio * canvas.clientWidth)
rect.y *= canvas.height / (displayPixelRatio * canvas.clientHeight)
rect.width *= canvas.width / (displayPixelRatio * canvas.clientWidth)
rect.height *= canvas.height / (displayPixelRatio * canvas.clientHeight)
if (!rect.normalized) {
if (rect.assumeClientRect) {
rect.x = Math.floor(rect.x * canvas.width / (displayPixelRatio * canvas.clientWidth))
rect.y = Math.floor(rect.y * canvas.height / (displayPixelRatio * canvas.clientHeight))
rect.width = Math.floor(rect.width * canvas.width / (displayPixelRatio * canvas.clientWidth))
rect.height = Math.floor(rect.height * canvas.height / (displayPixelRatio * canvas.clientHeight))
}
} else {
rect.x = Math.floor(rect.x * canvas.width)
rect.y = Math.floor(rect.y * canvas.height)
rect.width = Math.floor(rect.width * canvas.width)
rect.height = Math.floor(rect.height * canvas.height)
if (rect.assumeClientRect) {
console.warn('CanvasSnapshot: rect.assumeClientRect is ignored when rect is normalized')
}
}

destCanvas.width = Math.floor(rect.width * scale * displayPixelRatio)
destCanvas.height = Math.floor(rect.height * scale * displayPixelRatio)

const destCtx = destCanvas.getContext('2d')
if (!destCtx) {
console.error('snapshot: cannot create context')
@@ -66,8 +91,8 @@ export class CanvasSnapshot {
if (img.complete) resolve()
})
destCtx.drawImage(img,
img.width * rect.x * displayPixelRatio / canvas.width, img.height * rect.y * displayPixelRatio / canvas.height,
img.width * rect.width * displayPixelRatio / canvas.width, img.height * rect.height * displayPixelRatio / canvas.height,
Math.floor(img.width * rect.x * displayPixelRatio / canvas.width), Math.floor(img.height * rect.y * displayPixelRatio / canvas.height),
Math.floor(img.width * rect.width * displayPixelRatio / canvas.width), Math.floor(img.height * rect.height * displayPixelRatio / canvas.height),
0, 0,
destCanvas.width,
destCanvas.height,
@@ -81,7 +106,7 @@ export class CanvasSnapshot {

destCtx?.drawImage(
canvas,
rect.x * displayPixelRatio, rect.y * displayPixelRatio, rect.width * displayPixelRatio, rect.height * displayPixelRatio,
Math.floor(rect.x * displayPixelRatio), Math.floor(rect.y * displayPixelRatio), Math.floor(rect.width * displayPixelRatio), Math.floor(rect.height * displayPixelRatio),
0, 0, destCanvas.width, destCanvas.height,
)

@@ -103,7 +128,10 @@ export class CanvasSnapshot {
}

public static async GetDataUrl(canvas: HTMLCanvasElement, {mimeType = 'image/png', quality, ...options}: CanvasSnapshotOptions): Promise<string> {
const clone = options.cloneCanvas === false ? canvas : await this.GetClonedCanvas(canvas, options)
const doClone = isSafari() || options.cloneCanvas || options.rect || options.scale || options.displayPixelRatio
if (!doClone && (options.rect || options.scale || options.displayPixelRatio)) console.warn('CanvasSnapshot: rect, scale and displayPixelRatio are ignored when cloneCanvas is false')
const clone = !doClone ? canvas : await this.GetClonedCanvas(canvas, options)
// const clone = options.cloneCanvas === false ? canvas : await this.GetClonedCanvas(canvas, options)
const url = clone.toDataURL(mimeType, quality)
if (!this.Debug && clone !== canvas) clone.remove()
return url
@@ -121,12 +149,15 @@ export class CanvasSnapshot {
}

public static async GetBlob(canvas: HTMLCanvasElement, options: CanvasSnapshotOptions = {}): Promise<Blob> {
const clone = options.cloneCanvas === false ? canvas : await this.GetClonedCanvas(canvas, options)
const doClone = isSafari() || options.cloneCanvas || options.rect || options.scale || options.displayPixelRatio
if (!doClone && (options.rect || options.scale || options.displayPixelRatio)) console.warn('rect, scale and displayPixelRatio are ignored when cloneCanvas is false')
const clone = !doClone ? canvas : await this.GetClonedCanvas(canvas, options)
// const clone = options.cloneCanvas === false ? canvas : await this.GetClonedCanvas(canvas, options)

const blob = await new Promise<Blob>((resolve, reject) => {
clone.toBlob((b) => {
if (b) resolve(b)
else reject()
else reject(new Error('CanvasSnapshot Failed to export blob from canvas'))
}, options.mimeType ?? 'image/png', options.quality)
})
if (!this.Debug && clone !== canvas) clone.remove()
@@ -134,11 +165,43 @@ export class CanvasSnapshot {
return blob
}

public static async GetFile(canvas: HTMLCanvasElement, filename = 'image.png', options: CanvasSnapshotOptions = {}): Promise<File|string> {
return options.getDataUrl ? await this.GetDataUrl(canvas, options) : new File([await this.GetBlob(canvas, options)], filename, {
public static async GetFile(canvas: HTMLCanvasElement, filename = 'image', options: CanvasSnapshotOptions = {}): Promise<File|string> {
const suffix = '.' + (options.mimeType?.split('/')[1]?.toLowerCase() || 'png')
const fname = !filename.toLowerCase().endsWith(suffix) ? filename + suffix : filename
return options.getDataUrl ? await this.GetDataUrl(canvas, options) : new File([await this.GetBlob(canvas, options)], fname, {
type: options.mimeType ?? 'image/png',
lastModified: now(),
})
}

public static async GetTiledFiles(canvas: HTMLCanvasElement, filePrefix = 'image', tileRows = 2, tileCols = 2, options: CanvasSnapshotOptions = {}): Promise<(File|string)[]> {
const rect = options.rect ?? {x: 0, y: 0, width: 1, height: 1, assumeClientRect: false, normalized: true}

// rect.width *= options.displayPixelRatio ?? 1
// rect.height *= options.displayPixelRatio ?? 1

const files = []
for (let i = 0; i < tileCols; i++) {
for (let j = 0; j < tileRows; j++) {
const ext = options.mimeType?.split('/')[1] ?? 'png'
const file = await this.GetFile(canvas, `${filePrefix}_${i}_${j}.${ext}`, {
rect: {
x: rect.x + i * rect.width / tileCols,
y: rect.y + j * rect.height / tileRows,
width: rect.width / tileCols,
height: rect.height / tileRows,
assumeClientRect: rect.assumeClientRect,
normalized: rect.normalized,
},
}).catch(e => {
console.error(`CanvasSnapshot - Error exporting tiled file ${i}, ${j}`, e)
return null
})
if (file)
files.push(file)
}
}
return files
}

}

+ 8
- 3
website/plugin/CanvasSnapshotPlugin.md Просмотреть файл

@@ -17,7 +17,9 @@ next:
[Source Code](https://github.com/repalash/threepipe/blob/master/src/plugins/export/CanvasSnapshotPlugin.ts) &mdash;
[API Reference](https://threepipe.org/docs/classes/CanvasSnapshotPlugin.html)

Canvas Snapshot Plugin adds support for taking snapshots of the canvas and exporting them as images and data urls. It includes options to take snapshot of a region, mime type, quality render scale and scaling the output image. Check out the interface [CanvasSnapshotOptions](https://threepipe.org/docs/interfaces/CanvasSnapshotOptions.html) for more details.
Canvas Snapshot Plugin adds support for taking snapshots of the canvas and exporting them as images and data urls. It includes options to take snapshot of a region, mime type, quality render scale, tiled zip export for large resolution, scaling the output image, interfacing with SSAA, Progressive plugins etc.

Check out the interface [CanvasSnapshotOptions](https://threepipe.org/docs/interfaces/CanvasSnapshotOptions.html) for more details.

```typescript
import {ThreeViewer, CanvasSnapshotPlugin} from 'threepipe'
@@ -34,22 +36,25 @@ await snapshotPlugin.downloadSnapshot('image.webp', { // all parameters are opti
displayPixelRatio: 2, // render scale
mimeType: 'image/webp', // mime type of the image
waitForProgressive: true, // wait for progressive rendering to finish (ProgressivePlugin). true by default
progressiveFrames: 64, // number of frames to wait for progressive rendering to finish (ProgressivePlugin). 64 by default
rect: { // region to take snapshot. eg. crop center of the canvas
height: viewer.canvas.clientHeight / 2,
width: viewer.canvas.clientWidth / 2,
x: viewer.canvas.clientWidth / 4,
y: viewer.canvas.clientHeight / 4,
},
// tileRows: 3, // number of rows to tile the image. If more than one, a zip file will be exported
// tileColumns: 3, // number of columns to tile the image
})

// get data url (string)
const dataUrl = await snapshotPlugin.getDataUrl({ // all parameters are optional
const dataUrl: string = await snapshotPlugin.getDataUrl({ // all parameters are optional
displayPixelRatio: 2, // render scale
mimeType: 'image/webp', // mime type of the image
})

// get File
const file = await snapshotPlugin.getFile('file.jpeg', { // all parameters are optional
const file: File = await snapshotPlugin.getFile('file.jpeg', { // all parameters are optional
mimeType: 'image/jpeg', // mime type of the image
})
```

Загрузка…
Отмена
Сохранить