@itwin/frontend-devtools
Version:
Debug menu and supporting UI widgets
194 lines • 9.29 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Effects
*/
import { dispose } from "@itwin/core-bentley";
import { Point2d, Range1d, Range2d, Vector2d } from "@itwin/core-geometry";
import { TextureTransparency } from "@itwin/core-common";
import { GraphicType, imageElementFromUrl, IModelApp, ParticleCollectionBuilder, Tool, } from "@itwin/core-frontend";
import { parseToggle } from "../tools/parseToggle";
import { randomFloat, randomInteger } from "./Random";
/** The default snow effect parameters used by newly-created SnowDecorators. */
const defaultSnowParams = {
numParticles: 2000,
sizeRange: Range1d.createXX(3, 22),
transparencyRange: Range1d.createXX(0, 50),
velocityRange: new Range2d(-30, 50, 30, 130),
accelerationRange: new Range2d(-1, -0.25, 1, 0.25),
windVelocity: 0,
};
/** Simulates snowfall in a [Viewport]($frontend) using particle effects.
* @see [[SnowEffect]] for a [Tool]($frontend) that toggles this decorator.
* @see [ParticleCollectionBuilder]($frontend) for defining custom particle effects.
* @beta
*/
export class SnowDecorator {
/** The viewport being decorated. */
viewport;
/** Invoked when this decorator is to be destroyed. */
[Symbol.dispose];
/** The initial width and height of the viewport, from which we randomly select each particle's initial position. */
_dimensions;
/** The list of particles being drawn. */
_particles = [];
/** The image to display for each particle. */
_texture;
/** The last time `updateParticles()` was invoked, in milliseconds. */
_lastUpdateTime;
_params;
constructor(viewport, texture) {
this._params = { ...defaultSnowParams };
this.viewport = viewport;
this._dimensions = new Point2d(viewport.viewRect.width, viewport.viewRect.height);
this._lastUpdateTime = Date.now();
this._texture = texture;
// Tell the viewport to re-render the decorations every frame so that the snow particles animate smoothly.
const removeOnRender = viewport.onRender.addListener(() => viewport.invalidateDecorations());
// When the viewport is resized, replace this decorator with a new one to match the new dimensions.
const removeOnResized = viewport.onResized.addListener(() => {
// Transfer ownership of the texture to the new decorator.
const tex = this._texture;
this._texture = undefined;
this[Symbol.dispose]();
new SnowDecorator(viewport, tex);
});
// When the viewport is destroyed, dispose of this decorator too.
const removeOnDispose = viewport.onDisposed.addListener(() => this[Symbol.dispose]());
const removeDecorator = IModelApp.viewManager.addDecorator(this);
this[Symbol.dispose] = () => {
removeDecorator();
removeOnRender();
removeOnDispose();
removeOnResized();
this._texture = dispose(this._texture);
SnowDecorator._decorators.delete(viewport);
};
SnowDecorator._decorators.set(viewport, this);
// Initialize the particles.
for (let i = 0; i < this._params.numParticles; i++)
this._particles.push(this.emit(true));
}
decorate(context) {
if (context.viewport !== this.viewport || !this._texture)
return;
// Update the particles.
const now = Date.now();
const deltaMillis = now - this._lastUpdateTime;
this._lastUpdateTime = now;
this.updateParticles(deltaMillis / 1000);
// Create particle graphics.
const builder = ParticleCollectionBuilder.create({
viewport: this.viewport,
isViewCoords: true,
texture: this._texture,
size: (this._params.sizeRange.high - this._params.sizeRange.low) / 2,
});
for (const particle of this._particles)
builder.addParticle(particle);
const graphic = builder.finish();
if (graphic)
context.addDecoration(GraphicType.ViewOverlay, graphic);
}
/** Change some of the parameters affecting this decorator. */
configure(params) {
for (const key of Object.keys(params)) {
const val = params[key];
if (undefined !== val)
this._params[key] = val;
}
}
/** Emit a new particle with randomized properties. */
emit(randomizeHeight) {
return {
x: randomInteger(0, this._dimensions.x),
y: randomizeHeight ? randomInteger(0, this._dimensions.y) : 0,
z: 0,
size: randomInteger(this._params.sizeRange.low, this._params.sizeRange.high),
transparency: randomInteger(this._params.transparencyRange.low, this._params.transparencyRange.high),
velocity: new Vector2d(randomFloat(this._params.velocityRange.low.x, this._params.velocityRange.high.x), randomFloat(this._params.velocityRange.low.y, this._params.velocityRange.high.y)),
};
}
// Update the positions and velocities of all the particles based on the amount of time that has passed since the last update.
updateParticles(elapsedSeconds) {
// Determine if someone changed the desired number of particles.
const particleDiscrepancy = this._params.numParticles - this._particles.length;
if (particleDiscrepancy > 0) {
// Birth new particles up to the new maximum.
for (let i = 0; i < particleDiscrepancy; i++)
this._particles.push(this.emit(true));
}
else {
// Destroy extra particles.
this._particles.length = this._params.numParticles;
}
const acceleration = new Vector2d();
const velocity = new Vector2d();
for (let i = 0; i < this._particles.length; i++) {
// Apply some acceleration to produce random drift.
const particle = this._particles[i];
acceleration.set(randomFloat(this._params.accelerationRange.low.x, this._params.accelerationRange.high.x), randomFloat(this._params.accelerationRange.low.y, this._params.accelerationRange.high.y));
acceleration.scale(elapsedSeconds, acceleration);
particle.velocity.plus(acceleration, particle.velocity);
// Apply velocity.
particle.velocity.clone(velocity);
velocity.scale(elapsedSeconds, velocity);
particle.x += velocity.x;
particle.y += velocity.y;
// Apply wind
particle.x += this._params.windVelocity * elapsedSeconds;
// Particles that travel beyond the viewport's left or right edges wrap around to the other side.
if (particle.x < 0)
particle.x = this._dimensions.x - 1;
else if (particle.x >= this._dimensions.x)
particle.x = 0;
// Particles that travel beyond the viewport's bottom or top edges are replaced by newborn particles.
if (particle.y < 0 || particle.y >= this._dimensions.y)
this._particles[i] = this.emit(false);
}
}
static _decorators = new Map();
/** Toggle this decorator for the specified viewport.
* @param viewport The viewport to which the effect should be applied or removed.
* @param enable `true` to enable the effect, `false` to disable it, or `undefined` to toggle the current state.
*/
static async toggle(viewport, enable) {
const decorator = this._decorators.get(viewport);
if (undefined === enable)
enable = undefined === decorator;
if (undefined !== decorator && !enable)
decorator[Symbol.dispose]();
else if (undefined === decorator && enable) {
// Create a texture to use for the particles.
// Note: the decorator takes ownership of the texture, and disposes of it when the decorator is disposed.
const image = await imageElementFromUrl(`${IModelApp.publicPath}sprites/particle_snow.png`);
const texture = IModelApp.renderSystem.createTexture({
ownership: "external",
image: { source: image, transparency: TextureTransparency.Mixed },
});
new SnowDecorator(viewport, texture);
}
}
}
/** Toggles a decorator that simulates snow using particle effects.
* @see [[SnowDecorator]] for the implementation of the decorator.
* @beta
*/
export class SnowEffect extends Tool {
static toolId = "SnowEffect";
async run(enable) {
const vp = IModelApp.viewManager.selectedView;
if (vp)
await SnowDecorator.toggle(vp, enable);
return true;
}
async parseAndRun(...args) {
const enable = parseToggle(args[0]);
if (typeof enable !== "string")
await this.run(enable);
return true;
}
}
//# sourceMappingURL=Snow.js.map