Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Dimensions2d class #54

Merged
merged 29 commits into from
Jan 5, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
186a79a
Add Dimensions2d class
amacdonald-google Dec 15, 2020
03c524f
Port additional Toolbox components.
amacdonald-google Dec 15, 2020
eef4b7c
Consolidate and cull duplicated or similar functionality.
amacdonald-google Dec 17, 2020
7cb8ef4
Remove `getStyle`
amacdonald-google Dec 17, 2020
bb22936
Add comments, cull unnecessary code
amacdonald-google Dec 17, 2020
85d9b41
Handle 0 area canvases in WebGlImageSequence
amacdonald-google Dec 17, 2020
821fea8
Polish pass in response to review
amacdonald-google Dec 17, 2020
26c328d
Restore x
amacdonald-google Dec 18, 2020
1276663
Re-work Map classes
amacdonald-google Dec 18, 2020
f4a5c40
Cleanup arrayf
amacdonald-google Dec 18, 2020
4c353d0
Remove unused code
amacdonald-google Dec 18, 2020
6d5393f
Restore ArrayMap
amacdonald-google Dec 18, 2020
0569c8b
Remove unused code in CachedElementVector
amacdonald-google Dec 18, 2020
2731d26
Inline method ComputedStyleService
amacdonald-google Dec 18, 2020
93e665f
Extract duplicated code to ComputedStyleService
amacdonald-google Dec 18, 2020
c93396a
Remove unused Dimensions2dDom code
amacdonald-google Dec 18, 2020
f0bb5f9
Simplify Dimensions cached vector
amacdonald-google Dec 18, 2020
55e56b2
Simplify MatrixDom
amacdonald-google Dec 18, 2020
41c3a4f
Consolidate visible distance from root functions
amacdonald-google Dec 18, 2020
bbc0ccf
Inline single call init function
amacdonald-google Dec 18, 2020
6c394d7
Add comment explaining inspiration for DynamicDefaultMap
amacdonald-google Dec 19, 2020
de16706
Simplify visible-distance-from-root-service
amacdonald-google Dec 19, 2020
9624a3a
Yank abstract classes
amacdonald-google Dec 19, 2020
937735b
Remove a lot of caching classes.
amacdonald-google Dec 19, 2020
726a46e
Remove caching
amacdonald-google Dec 19, 2020
f65bd63
Correct comment
amacdonald-google Dec 19, 2020
2f16261
Remove now unused code
amacdonald-google Dec 19, 2020
c7bf8f5
Respond to review feedback with improvements
amacdonald-google Dec 19, 2020
5debf79
Address feedback from PR
amacdonald-google Jan 5, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/arrayf/arrayf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,45 @@ export class arrayf {
return maxValue;
}

/**
* Determines if the value in each index of each list matches the
* corresponding value in that same index in each other list.
* @param lists
*/
static areArrayValuesIdentical<T>(lists: T[][]): boolean {
angusm marked this conversation as resolved.
Show resolved Hide resolved
angusm marked this conversation as resolved.
Show resolved Hide resolved
// Short circuit on null values
if (!lists.every((list: T[]) => list instanceof Array)) {
return false;
}
// Check all lists have same lengths
const lengths = lists.map((list: T[]) => list.length);
const haveIdenticalLengths = arrayf.containsIdenticalValues(lengths);
if (!haveIdenticalLengths) {
return false;
}
// Verify that values match across lists
return arrayf.zip(...lists)
.every((zippedValues: T[]) => {
return arrayf.containsIdenticalValues(zippedValues);
});
}

/**
* Determine if each value in the array is the same value.
*/
static containsIdenticalValues<T>(values: T[]): boolean {
return values.length === 0 ||
values.every((value) => values[0] === value);
}

/**
* Removes the first instance of a value from the given array.
* Useful when tracking the places a singleton or service is used so that
* it can dispose of itself when no longer needed.
*/
static removeFirstInstance<T>(values: T[], value: T): T[] {
angusm marked this conversation as resolved.
Show resolved Hide resolved
return [
...values.slice(0, values.indexOf(value)),
...values.slice(values.indexOf(value) + 1)];
}
}
157 changes: 157 additions & 0 deletions src/dom/cached-element-vector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* CachedElementVector is used to optimize access to commonly checked values.
angusm marked this conversation as resolved.
Show resolved Hide resolved
* Abstracts the caching of values such as window dimensions or scroll positions
* that can be stored as vectors.
*/
import { DynamicDefaultMap } from '../map/dynamic-default';
import { MultiValueDynamicDefaultMap } from '../map/multi-value-dynamic-default';
import { MultiDimensionalVector } from '../mathf/geometry/multi-dimensional-vector';
import { Raf } from '..';

// Number of past values to cache
const VALUE_LIMIT: number = 2;

type InnerCache = MultiValueDynamicDefaultMap<any, any>;

/**
* All instances of CachedElementVector will share a single cache to facilitate
* abstraction of the cache checking/clearing.
*
* Also helps ensure that only singletons are being created by checking for
* existing instances in this cache.
*/
const caches: DynamicDefaultMap<any, InnerCache> =
DynamicDefaultMap.usingFunction<any, any>(
angusm marked this conversation as resolved.
Show resolved Hide resolved
(Class) => {
return MultiValueDynamicDefaultMap.usingFunction(
(args: any[]) => new Class(...args));
});

/**
* Tracks where singletons are being used so that they are not fully disposed
* if they are being used elsewhere.
*/
const uses: DynamicDefaultMap<CachedElementVector<MultiDimensionalVector>, Set<any>> =
DynamicDefaultMap.usingFunction(() => new Set());

export abstract class CachedElementVector<T extends MultiDimensionalVector> {
/**
* Used when a single type of caching could serve multiple elements.
* For instance you may want to cache the scroll of the main document but also
* the scroll of an inner scrollable element.
*/
static getForElement(use: any, args: any[] = [null]): any {
const instance = caches.get(this).get(args);
uses.get(instance).add(use);
return instance;
}

/**
* Returns a generic singleton.
*/
static getSingleton(use: any): any {
return this.getForElement(use);
}

/**
* Type of vector this is caching, Dimensions/Vector2d/etc.
*/
protected static VectorClass: typeof MultiDimensionalVector =
MultiDimensionalVector;

// Element whose values are being cached.
protected element: HTMLElement;

// Optional args the constructor was called with.
private readonly args: any[];

// Currently cached values
private values: T[];

// Allows caches to persist for a little bit so they aren't being created
// and destroyed every frame by short-lived calls.
private disposeTimeout: number;

private raf: Raf;

protected constructor(element: any = null, ...args: any[]) {
const instanceByElement = caches.get(this.constructor);
this.raf = new Raf(() => this.loop());
this.args = [element, ...args];

// Prevent direct instantiation
if (instanceByElement.has([element, ...args])) {
if (element) {
throw new Error('Please use getForElement instead of new.');
} else {
throw new Error('Please use getSingleton instead of new.');
}
}

this.disposeTimeout = null;
this.element = element;
this.values = <T[]>[this.getCurrentVector()];
this.init();
}

getLastValue(): T {
return this.values.slice(-1)[0];
}

getDelta(): T {
const values = this.getCurrentAndLastValue();
return <T>this.getVectorClass().subtract(values[0], values[1]);
}

hasChanged(): boolean {
return !this.getVectorClass().areEqual(...this.getCurrentAndLastValue());
}

dispose(use: any): void {
uses.get(this).delete(use);
clearTimeout(this.disposeTimeout);
// Hang tight for a minute so we aren't creating and destroying these caches
// too rapidly.
this.disposeTimeout = window.setTimeout(
() => {
if (uses.size <= 0) {
caches.get(this.constructor).delete(this.args);
this.raf.dispose();
}
},
1000);
}

// Expected to be overridden
protected getValues(): number[] {
throw new Error('getValues must be overridden by child class');
}

private init(): void {
this.raf.start();
angusm marked this conversation as resolved.
Show resolved Hide resolved
}

private getVectorClass(): typeof MultiDimensionalVector {
return (<typeof CachedElementVector>this.constructor).VectorClass;
}

private getCurrentVector(): T {
return <T>new (this.getVectorClass())(...this.getValues());
}

// Measure at the start of every frame
private loop(): void {
this.raf.preRead(() => this.measureValues());
}

private measureValues(): void {
this.values =
this.values
.slice(-(VALUE_LIMIT - 1))
.concat([this.getCurrentVector()]);
}

private getCurrentAndLastValue(): MultiDimensionalVector[] {
return this.values.slice(-2);
}
}
70 changes: 70 additions & 0 deletions src/dom/computed-style-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { DynamicDefaultMap } from '../map/dynamic-default';
import { Raf } from '..';
import { arrayf } from '../arrayf/arrayf';

/**
* Caches results of getComputedStyle on a per-frame basis.
* It can be an expensive operation and will typically force style
* recalc or layout, so it can be extra helpful in minimizing potential trhasing
* in situations where there may be code running outside of appropriate raf
* read/write calls.
*/
export class ComputedStyleService {
static getSingleton(use: any): ComputedStyleService {
this.singleton = this.singleton || new this();
this.singleton.uses.push(use);
return this.singleton;
}

private static singleton: ComputedStyleService = null;
private readonly raf: Raf;

// DynamicDefaultMap serves as the cache, generating values and storing them
// Gets cleared out during the postWrite step.
private computedStyle: DynamicDefaultMap<Element, CSSStyleDeclaration>;

// Track where this is used so it can be destroyed as needed;
private uses: any[];

// Allows singleton to persist for a little bit so it's not being created
// and destroyed every frame by short-lived calls.
private disposeTimeout: number;

constructor() {
this.uses = [];
this.disposeTimeout = null;
this.raf = new Raf(() => this.loop());
this.computedStyle =
DynamicDefaultMap.usingFunction(
(element: Element) => window.getComputedStyle(element));
this.init();
}

getComputedStyle(element: Element) {
return this.computedStyle.get(element);
}

dispose(use: any) {
this.uses = arrayf.removeFirstInstance(this.uses, use);
// Hang tight for a minute so we aren't creating and destroying the
// singleton too rapidly.
angusm marked this conversation as resolved.
Show resolved Hide resolved
clearTimeout(this.disposeTimeout);
this.disposeTimeout = window.setTimeout(
() => {
if (this.uses.length <= 0) {
ComputedStyleService.singleton = null;
this.raf.dispose();
this.computedStyle.clear();
}
},
1000);
}

private init() {
this.raf.start();
}

private loop() {
this.raf.postWrite(() => this.computedStyle.clear());
}
angusm marked this conversation as resolved.
Show resolved Hide resolved
}
21 changes: 17 additions & 4 deletions src/dom/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Defer } from '../func/defer';
import { func } from '../func/func';
import { is } from '../is/is';
import { DomWatcher } from '../dom/dom-watcher';
import { ComputedStyleService } from './computed-style-service';

/**
* Yano DOM utility functions.
Expand Down Expand Up @@ -477,7 +478,11 @@ export class dom {
* @return CSSStyleDeclartion
*/
static getComputedStyle(element: HTMLElement): CSSStyleDeclaration {
return window.getComputedStyle(element);
// Use per-frame cached values to avoid style-recalc and layout calls
const styleService = ComputedStyleService.getSingleton(this);
const value = styleService.getComputedStyle(element);
styleService.dispose(this);
return value;
}


Expand Down Expand Up @@ -850,15 +855,19 @@ export class dom {
* ```
*/
static getStyle(el: Element): CSSStyleDeclaration {
return el['currentStyle'] || window.getComputedStyle(el);
// Use per-frame cached values to avoid style-recalc and layout calls
const styleService = ComputedStyleService.getSingleton(this);
const value = styleService.getComputedStyle(el);
styleService.dispose(this);
return value;
}


/**
* Tests whether the provided element is set to display none.
*/
static isDisplayNone(el: Element): boolean {
let style = window.getComputedStyle(el, null).display;
// Use internal style fetching to allow for optimizations
let style = dom.getStyle(el).display;
return style == 'none';
}

Expand Down Expand Up @@ -985,4 +994,8 @@ export class dom {
})

}

static getScrollElement(): Element {
return document.scrollingElement || document.documentElement;
}
}
53 changes: 53 additions & 0 deletions src/dom/position/dimensions-2d-dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Dimensions2d } from '../../mathf/geometry/dimensions-2d';
import { dom } from '../dom';

/**
* Version of Dimenions2d with various DOM specific helper functions.
*/
export class Dimensions2dDom extends Dimensions2d {
static fromCanvas<T extends Dimensions2dDom>(
element: HTMLCanvasElement = null
): T {
return <T>new this(element.width, element.height);
}

static fromVideo<T extends Dimensions2dDom>(
element: HTMLVideoElement = null
): T {
return <T>new this(element.videoWidth, element.videoHeight);
}

static fromElementOffset<T extends Dimensions2dDom>(
element: HTMLElement = null
): T {
if (element) {
return <T>new this(element.offsetWidth, element.offsetHeight);
} else {
return this.fromInnerWindow();
}
}

static fromRootElement<T extends Dimensions2dDom>() {
return <T>new this(
document.documentElement.clientWidth,
document.documentElement.clientHeight);
}

static fromScrollElementClient<T extends Dimensions2dDom>() {
return <T>new this(
dom.getScrollElement().clientWidth,
dom.getScrollElement().clientHeight);
}

static fromInnerWindow<T extends Dimensions2dDom>() {
return <T>new this(window.innerWidth, window.innerHeight);
}

/**
* Style the given element with the width and height from this instance
*/
sizeElement(element: HTMLElement): void {
element.style.width = `${this.getWidth()}px`;
element.style.height = `${this.getHeight()}px`;
}
}
Loading