Skip to content
Snippets Groups Projects
Commit 4801435d authored by Alexander Rose's avatar Alexander Rose
Browse files

add controls to create image

parent 33fd105e
No related branches found
No related tags found
No related merge requests found
......@@ -4,16 +4,57 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
/** resize canvas to container element */
/** Set canvas size taking `devicePixelRatio` into account */
export function setCanvasSize(canvas: HTMLCanvasElement, width: number, height: number) {
canvas.width = window.devicePixelRatio * width
canvas.height = window.devicePixelRatio * height
Object.assign(canvas.style, { width: `${width}px`, height: `${height}px` })
}
/** Resize canvas to container element taking `devicePixelRatio` into account */
export function resizeCanvas (canvas: HTMLCanvasElement, container: Element) {
let w = window.innerWidth
let h = window.innerHeight
let width = window.innerWidth
let height = window.innerHeight
if (container !== document.body) {
let bounds = container.getBoundingClientRect()
w = bounds.right - bounds.left
h = bounds.bottom - bounds.top
width = bounds.right - bounds.left
height = bounds.bottom - bounds.top
}
setCanvasSize(canvas, width, height)
}
function _canvasToBlob(canvas: HTMLCanvasElement, callback: BlobCallback, type?: string, quality?: any) {
const bin = atob(canvas.toDataURL(type, quality).split(',')[1])
const len = bin.length
const len32 = len >> 2
const a8 = new Uint8Array(len)
const a32 = new Uint32Array( a8.buffer, 0, len32 )
let j = 0
for (let i = 0; i < len32; ++i) {
a32[i] = bin.charCodeAt(j++) |
bin.charCodeAt(j++) << 8 |
bin.charCodeAt(j++) << 16 |
bin.charCodeAt(j++) << 24
}
let tailLength = len & 3;
while (tailLength--) a8[j] = bin.charCodeAt(j++)
callback(new Blob([a8], { type: type || 'image/png' }));
}
export async function canvasToBlob(canvas: HTMLCanvasElement, type?: string, quality?: any): Promise<Blob> {
return new Promise((resolve, reject) => {
const callback = (blob: Blob | null) => {
if (blob) resolve(blob)
else reject('no blob returned')
}
if (!HTMLCanvasElement.prototype.toBlob) {
_canvasToBlob(canvas, callback, type, quality)
} else {
canvas.toBlob(callback, type, quality)
}
canvas.width = window.devicePixelRatio * w
canvas.height = window.devicePixelRatio * h
Object.assign(canvas.style, { width: `${w}px`, height: `${h}px` })
})
}
\ No newline at end of file
......@@ -190,6 +190,7 @@ export interface WebGLContext {
readonly framebufferCache: FramebufferCache
readonly maxTextureSize: number
readonly maxRenderbufferSize: number
readonly maxDrawBuffers: number
unbindFramebuffer: () => void
......@@ -212,6 +213,7 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
const parameters = {
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
maxRenderbufferSize: gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) as number,
maxDrawBuffers: isWebGL2(gl) ? gl.getParameter(gl.MAX_DRAW_BUFFERS) as number : 0,
maxVertexTextureImageUnits: gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) as number,
}
......@@ -274,6 +276,7 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
framebufferCache,
get maxTextureSize () { return parameters.maxTextureSize },
get maxRenderbufferSize () { return parameters.maxRenderbufferSize },
get maxDrawBuffers () { return parameters.maxDrawBuffers },
unbindFramebuffer: () => unbindFramebuffer(gl),
......
......@@ -363,3 +363,24 @@
}
}
}
.msp-image-preview {
position: relative;
background: $default-background;
margin-top: 1px;
display: flex;
justify-content: center;
> canvas {
max-height: 200px;
border-width: 0px 1px 0px 1px;
border-style: solid;
border-color: $border-color;
background-color: $default-background;
background-image: linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey),
linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
}
}
\ No newline at end of file
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as React from 'react';
import { CollapsableControls, CollapsableState } from './base';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ParameterControls } from './controls/parameters';
import { ImagePass } from '../../mol-canvas3d/passes/image';
import { download } from '../../mol-util/download';
import { setCanvasSize, canvasToBlob } from '../../mol-canvas3d/util';
import { Task } from '../../mol-task';
interface ImageControlsState extends CollapsableState {
showPreview: boolean
size: 'canvas' | 'custom'
width: number
height: number
}
const maxWidthUi = 260
const maxHeightUi = 180
export class ImageControls<P, S extends ImageControlsState> extends CollapsableControls<P, S> {
private canvasRef = React.createRef<HTMLCanvasElement>()
private canvas: HTMLCanvasElement
private canvasContext: CanvasRenderingContext2D
private imagePass: ImagePass
constructor(props: P, context?: any) {
super(props, context)
this.subscribe(this.plugin.events.canvas3d.initialized, () => this.forceUpdate())
}
private getSize() {
return this.state.size === 'canvas' ? {
width: this.plugin.canvas3d.webgl.gl.drawingBufferWidth,
height: this.plugin.canvas3d.webgl.gl.drawingBufferHeight
} : {
width: this.state.width,
height: this.state.height
}
}
private preview = () => {
const { width, height } = this.getSize()
if (width <= 0 || height <= 0) return
let w: number, h: number
const aH = maxHeightUi / height
const aW = maxWidthUi / width
if (aH < aW) {
h = Math.round(Math.min(maxHeightUi, height))
w = Math.round(width * (h / height))
} else {
w = Math.round(Math.min(maxWidthUi, width))
h = Math.round(height * (w / width))
}
setCanvasSize(this.canvas, w, h)
const { pixelRatio } = this.plugin.canvas3d.webgl
const imageData = this.imagePass.getImageData(w * pixelRatio, h * pixelRatio)
this.canvasContext.putImageData(imageData, 0, 0)
}
private downloadTask = () => {
return Task.create('Download Image', async ctx => {
const { width, height } = this.getSize()
if (width <= 0 || height <= 0) return
await ctx.update('Rendering image...')
const imageData = this.imagePass.getImageData(width, height)
await ctx.update('Encoding image...')
const canvas = document.createElement('canvas')
canvas.width = imageData.width
canvas.height = imageData.height
const canvasCtx = canvas.getContext('2d')
if (!canvasCtx) throw new Error('Could not create canvas 2d context')
canvasCtx.putImageData(imageData, 0, 0)
await ctx.update('Downloading image...')
const blob = await canvasToBlob(canvas)
download(blob, 'molstar-image')
})
}
private download = () => {
this.plugin.runTask(this.downloadTask())
}
private syncCanvas() {
if (!this.canvasRef.current) return
if (this.canvasRef.current === this.canvas) return
this.canvas = this.canvasRef.current
const ctx = this.canvas.getContext('2d')
if (!ctx) throw new Error('Could not get canvas 2d context')
this.canvasContext = ctx
}
private handlePreview() {
if (this.state.showPreview) {
this.syncCanvas()
this.preview()
}
}
componentDidUpdate() {
this.handlePreview()
}
componentDidMount() {
this.imagePass = this.plugin.canvas3d.getImagePass()
this.imagePass.setProps({
multiSample: { mode: 'on', sampleLevel: 2 },
postprocessing: this.plugin.canvas3d.props.postprocessing
})
this.handlePreview()
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => {
this.imagePass.setProps({
multiSample: { mode: 'on', sampleLevel: 2 },
postprocessing: this.plugin.canvas3d.props.postprocessing
})
this.handlePreview()
})
this.subscribe(this.plugin.canvas3d.didDraw, () => this.handlePreview())
}
private togglePreview = () => this.setState({ showPreview: !this.state.showPreview })
private setProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
if (p.name === 'size') {
if (p.value.name === 'custom') {
this.setState({ size: p.value.name, width: p.value.params.width, height: p.value.params.height })
} else {
this.setState({ size: p.value.name })
}
}
}
private get params () {
const max = Math.min(this.plugin.canvas3d ? this.plugin.canvas3d.webgl.maxRenderbufferSize : 4096, 8192)
const { width, height } = this.defaultState()
return {
size: PD.MappedStatic('custom', {
canvas: PD.Group({}),
custom: PD.Group({
width: PD.Numeric(width, { min: 1, max, step: 1 }),
height: PD.Numeric(height, { min: 1, max, step: 1 }),
}, { isFlat: true })
}, { options: [['canvas', 'Canvas'], ['custom', 'Custom']] })
}
}
private get values () {
return this.state.size === 'canvas'
? { size: { name: 'canvas', params: {} } }
: { size: { name: 'custom', params: { width: this.state.width, height: this.state.height } } }
}
protected defaultState() {
return {
isCollapsed: false,
header: 'Create Image',
showPreview: false,
size: 'canvas',
width: 1920,
height: 1080
} as S
}
protected renderControls() {
return <div>
<div className='msp-control-row'>
<button className='msp-btn msp-btn-block' onClick={this.download}>Download</button>
</div>
<ParameterControls params={this.params} values={this.values} onChange={this.setProps} />
<div className='msp-control-group-wrapper'>
<div className='msp-control-group-header'>
<button className='msp-btn msp-btn-block' onClick={this.togglePreview}>
<span className={`msp-icon msp-icon-${this.state.showPreview ? 'collapse' : 'expand'}`} />
Preview
</button>
</div>
{this.state.showPreview && <div className='msp-control-offset'>
<div className='msp-image-preview'>
<canvas width='0px' height='0px' ref={this.canvasRef} />
</div>
</div>}
</div>
</div>
}
}
\ No newline at end of file
......@@ -22,6 +22,7 @@ import { StateTransform } from '../../mol-state';
import { UpdateTransformControl } from './state/update-transform';
import { SequenceView } from './sequence';
import { Toasts } from './toast';
import { ImageControls } from './image';
export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) {
......@@ -129,6 +130,7 @@ export class ControlsWrapper extends PluginUIComponent {
{/* <AnimationControlsWrapper /> */}
{/* <CameraSnapshots /> */}
<StructureToolsWrapper />
<ImageControls />
<StateSnapshots />
</div>;
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment