@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
305 lines (205 loc) • 8.08 kB
JavaScript
import { assert } from "../../../../core/assert.js";
import List from "../../../../core/collection/list/List.js";
import Vector1 from "../../../../core/geom/Vector1.js";
import { clamp01 } from "../../../../core/math/clamp01.js";
import ListView from "../../../common/ListView.js";
import { DomSizeObserver } from "../../../util/DomSizeObserver.js";
import View from "../../../View.js";
import { CanvasView } from "../../CanvasView.js";
import EmptyView from "../../EmptyView.js";
import { RESOURCE_BAR_SEGMENTS } from "./RESOURCE_BAR_SEGMENTS.js";
export class SegmentedResourceBarView extends View {
/**
*
* @param {(Vector1|ObservedInteger)[]} values
* @param {Vector1|ObservedInteger|ReactiveExpression} [max=1]
* @param {string[]} [classList]
*/
constructor({
values,
max = Vector1.one,
classList = []
}) {
super();
assert.isArray(values, 'values');
assert.defined(max, 'max');
/**
*
* @type {(Vector1|ObservedInteger)[]}
* @private
*/
this.__values_current = values;
/**
*
* @type {Vector1|ObservedInteger}
* @private
*/
this.__value_max = max;
/**
*
* @type {List<NumericInterval>}
*/
this.highlights = new List();
this.el = document.createElement('div');
this.addClass('ui-segmented-resource-bar-view');
this.addClasses(classList);
// build structure
this.__v__fill = new EmptyView({ classList: ['fill-container'] });
this.addChild(this.__v__fill);
this.__value_fills = [];
for (let i = 0; i < this.__values_current.length; i++) {
const v_value = new EmptyView({ classList: ['fill', `value-${i}`] });
this.__v__fill.addChild(v_value);
this.__value_fills[i] = v_value;
}
this.__v__ghost = new EmptyView({ classList: ['ghost'] });
this.addChild(this.__v__ghost);
this.__v_segments = new CanvasView();
this.__v_segments.addClass('notch-overlay');
this.addChild(this.__v_segments);
/**
*
* @type {ListView}
*/
this.highlightViews = new ListView(this.highlights, {
classList: ['highlights'],
elementFactory: (interval) => {
const v = new EmptyView({ classList: ['highlight'] });
const valueMax = this.__value_max;
const update = () => {
const m = valueMax.getValue();
const v0 = interval.min / m;
const span = (interval.max - interval.min) / m;
const style = v.el.style;
style.left = `${v0 * 100}%`;
style.width = `${span * 100}%`;
};
v.bindSignal(interval.onChanged, update);
const valuesCurrent = this.__values_current;
for (let i = 0; i < valuesCurrent.length; i++) {
const valueCurrent = valuesCurrent[i];
v.bindSignal(valueCurrent.onChanged, update);
}
v.bindSignal(valueMax.onChanged, update);
v.on.linked.add(update);
return v;
}
});
this.addChild(this.highlightViews);
// subscribe to changes
for (let i = 0; i < this.__values_current.length; i++) {
this.bindSignal(this.__values_current[i].onChanged, this.updateFill, this);
}
this.bindSignal(this.__value_max.onChanged, this.handleMaxChange, this);
/**
* We need an observer in order to know when we need to resize the canvas element that holds marks
* @type {DomSizeObserver}
* @private
*/
this.__sizeObserver = new DomSizeObserver();
this.__sizeObserver.attach(this.el);
this.__sizeObserver.dimensions.size.onChanged.add(this.__updateSize, this);
}
link() {
super.link();
this.__sizeObserver.start();
this.update();
}
unlink() {
this.__sizeObserver.stop();
super.unlink();
}
update() {
this.updateFill();
this.updateSegments();
}
__updateSize() {
if (this.__sizeObserver.dimensions.size.distanceSqrTo(this.__v_segments.size) < 0.5) {
// change too small, ignore
return;
}
this.updateSegments();
}
handleMaxChange() {
this.updateSegments();
this.updateFill();
}
updateSegments() {
/**
*
* @type {Vector2}
*/
const size = this.__sizeObserver.dimensions.size;
this.__v_segments.size.copy(size);
const vSegments = this.__v_segments;
vSegments.clear();
/**
*
* @type {CanvasRenderingContext2D}
*/
const ctx = vSegments.context2d;
const maxValue = this.__value_max.getValue();
// figure out what segments to use
let major_segment_index = 0;
const max_segment_index = RESOURCE_BAR_SEGMENTS.length - 1;
for (let i = max_segment_index; i >= 0; i--) {
const segmentDefinition = RESOURCE_BAR_SEGMENTS[i];
if (segmentDefinition.value <= maxValue && (i >= max_segment_index || RESOURCE_BAR_SEGMENTS[i + 1].value > maxValue)) {
//we found major segment
major_segment_index = i;
}
}
const major_spec = RESOURCE_BAR_SEGMENTS[major_segment_index];
const minor_spec = RESOURCE_BAR_SEGMENTS[major_segment_index - 1];
const majorSegmentValue = major_spec.value;
if (minor_spec === undefined) {
//no minor spec, only paint major
/**
*
* @type {number}
*/
const stepCount = maxValue / majorSegmentValue;
const segmentWidth = size.x / stepCount;
for (let i = 1; i < stepCount; i++) {
const x = i * segmentWidth;
// major notch
major_spec.draw(ctx, x, size.y);
}
} else {
const minorSegmentValue = minor_spec.value;
const majorMultiplier = majorSegmentValue / minorSegmentValue;
/**
*
* @type {number}
*/
const stepCount = maxValue / minorSegmentValue;
const minorSegmentWidth = size.x / stepCount;
const displayMinor = minorSegmentWidth >= 2;
for (let i = 1; i < stepCount; i++) {
const x = i * minorSegmentWidth;
if (i % majorMultiplier === 0) {
// major notch
major_spec.draw(ctx, x, size.y);
} else if (displayMinor) {
// minor notch
minor_spec.draw(ctx, x, size.y);
}
}
}
}
updateFill() {
const m = this.__value_max.getValue();
let total_current = 0;
for (let i = 0; i < this.__values_current.length; i++) {
const v = this.__values_current[i];
const value = v.getValue();
total_current += value;
const v_fraction = clamp01(v / m);
const fill_view = this.__value_fills[i];
fill_view.el.style.width = `${v_fraction * 100}%`;
}
const f = clamp01(total_current / m);
const width = `${f * 100}%`;
this.__v__ghost.el.style.width = width;
}
}