2025-03-25 06:42:27 +00:00
|
|
|
|
|
|
|
|
|
|
|
import { ClassFlag } from "./ClassFlag";
|
|
|
|
import { HTMLNodeTreeWalker } from '../graphs/HTMLNodeTreeWalker';
|
|
|
|
import { Box2 } from "../geometry/Box2";
|
|
|
|
import { Vector2 } from "../geometry/Vector2";
|
|
|
|
import { ElementAttribute } from "./ElementAttribute";
|
|
|
|
import { MapList } from "../tools/MapList";
|
|
|
|
|
|
|
|
export type above_sight = "above-sight";
|
|
|
|
export type in_sight = "in-sight";
|
|
|
|
export type below_sight = "below-sight";
|
|
|
|
|
|
|
|
export type InsighStateType = above_sight | in_sight | below_sight;
|
|
|
|
|
|
|
|
export class InsighState
|
|
|
|
{
|
|
|
|
static readonly above_sight:above_sight = "above-sight";
|
|
|
|
static readonly in_sight:in_sight = "in-sight";
|
|
|
|
static readonly below_sight:below_sight = "below-sight";
|
|
|
|
}
|
|
|
|
|
|
|
|
export class InsightData
|
|
|
|
{
|
|
|
|
insight:Insight;
|
|
|
|
isInsight = false;
|
|
|
|
wasInsight = false;
|
|
|
|
viewBox:Box2 = null;
|
2025-09-06 11:33:04 +00:00
|
|
|
elementBox:Box2 = Box2.polar( 0 );
|
2025-03-25 06:42:27 +00:00
|
|
|
changeTime:number = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class InsightElement extends Element
|
|
|
|
{
|
|
|
|
|
|
|
|
_insightData:InsightData;
|
|
|
|
|
|
|
|
static ensure( element:Element )
|
|
|
|
{
|
|
|
|
let insightElement = element as InsightElement;
|
|
|
|
|
|
|
|
if ( ! insightElement._insightData )
|
|
|
|
{
|
|
|
|
insightElement._insightData = new InsightData();
|
|
|
|
}
|
|
|
|
|
|
|
|
return insightElement;
|
|
|
|
}
|
|
|
|
|
|
|
|
static forAllInsightElements( root:Element, callback:( insightElement:InsightElement )=>void )
|
|
|
|
{
|
|
|
|
let walker = HTMLNodeTreeWalker.$;
|
|
|
|
|
|
|
|
walker.forAll( root,
|
|
|
|
n =>
|
|
|
|
{
|
|
|
|
let insightElement = n as InsightElement;
|
|
|
|
|
|
|
|
if ( insightElement._insightData )
|
|
|
|
{
|
|
|
|
callback( insightElement );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export type InsightCallback = ()=>void;
|
|
|
|
export type CallbackMapList = Map<Element,InsightCallback[]>;
|
|
|
|
|
|
|
|
|
|
|
|
export class Insight
|
|
|
|
{
|
|
|
|
static attribute = new ElementAttribute( "insight" );
|
|
|
|
static wasSeenFlag = new ClassFlag( "was-in-sight" );
|
|
|
|
static maxHeight = new ElementAttribute( "max-insight-height" );
|
|
|
|
|
|
|
|
static readonly above_sight = new ClassFlag( InsighState.above_sight );
|
|
|
|
static readonly in_sight = new ClassFlag( InsighState.in_sight );
|
|
|
|
static readonly below_sight = new ClassFlag( InsighState.below_sight );
|
|
|
|
|
|
|
|
static changeFrequencyMS = 200;
|
|
|
|
private _elements:Element[] = [];
|
|
|
|
private _offsightCallbackElements:CallbackMapList;
|
|
|
|
private _insightCallbackElements:CallbackMapList;
|
|
|
|
|
|
|
|
private _viewBoxSize:Vector2 = null;
|
|
|
|
private _hasScreenUpdate = false;
|
|
|
|
private _lastYOffset = 0;
|
|
|
|
private _yTresholdForChanges = 15;
|
|
|
|
private _lastGrabbedTime = 0;
|
|
|
|
private _afterGrabingUpdateDurationMS = 3000;
|
|
|
|
|
|
|
|
static _allInstances:Insight[] = [];
|
|
|
|
|
|
|
|
static updateAll()
|
|
|
|
{
|
|
|
|
this._allInstances.forEach( i => { i._hasScreenUpdate = true; i.update() } );
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor( publishGlobally:boolean = true )
|
|
|
|
{
|
|
|
|
if ( publishGlobally )
|
|
|
|
{
|
|
|
|
Insight._allInstances.push( this );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onInsight( element:Element, callback:InsightCallback )
|
|
|
|
{
|
|
|
|
MapList.add( this._insightCallbackElements, element, callback );
|
|
|
|
}
|
|
|
|
|
|
|
|
onOffsight( element:Element, callback:InsightCallback )
|
|
|
|
{
|
|
|
|
MapList.add( this._offsightCallbackElements, element, callback );
|
|
|
|
}
|
|
|
|
|
|
|
|
grabElements()
|
|
|
|
{
|
|
|
|
this._lastGrabbedTime = new Date().getTime();
|
|
|
|
|
|
|
|
this._elements.forEach(
|
|
|
|
e =>
|
|
|
|
{
|
|
|
|
let element = InsightElement.ensure( e );
|
|
|
|
element._insightData.insight = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
this._elements = Insight.attribute.queryAll( document.body );
|
|
|
|
this._offsightCallbackElements = new Map<Element,InsightCallback[]>();
|
|
|
|
this._insightCallbackElements = new Map<Element,InsightCallback[]>();
|
|
|
|
|
|
|
|
this._elements.forEach(
|
|
|
|
e =>
|
|
|
|
{
|
|
|
|
let element = InsightElement.ensure( e );
|
|
|
|
element._insightData.insight = this;
|
|
|
|
}
|
|
|
|
|
|
|
|
);
|
|
|
|
|
2025-09-06 11:33:04 +00:00
|
|
|
console.log( this._elements );
|
2025-03-25 06:42:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
update()
|
|
|
|
{
|
|
|
|
this._updateViewBox();
|
|
|
|
|
|
|
|
let currentTime = new Date().getTime();
|
|
|
|
let elapsedSinceGrabbing = currentTime - this._lastGrabbedTime;
|
|
|
|
|
|
|
|
if ( elapsedSinceGrabbing < this._afterGrabingUpdateDurationMS )
|
|
|
|
{
|
|
|
|
this._hasScreenUpdate = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let yChange = window.pageYOffset - this._lastYOffset;
|
|
|
|
|
|
|
|
if ( Math.abs( yChange ) > this._yTresholdForChanges )
|
|
|
|
{
|
|
|
|
this._hasScreenUpdate = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( ! this._hasScreenUpdate )
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._lastYOffset = window.pageYOffset;
|
|
|
|
this._hasScreenUpdate = false;
|
|
|
|
|
|
|
|
let now = new Date().getTime();
|
|
|
|
this._elements.forEach( e => this.updateElement( e, now ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
updateElement( e:Element, updateTime:number )
|
|
|
|
{
|
|
|
|
let insightElement = InsightElement.ensure( e );
|
|
|
|
let insightData = insightElement._insightData;
|
|
|
|
|
|
|
|
let viewBox = insightData.viewBox;
|
2025-09-06 11:33:04 +00:00
|
|
|
|
|
|
|
// console.log( viewBox );
|
2025-03-25 06:42:27 +00:00
|
|
|
|
|
|
|
if ( ! viewBox )
|
|
|
|
{
|
|
|
|
viewBox = this._getViewBox( e );
|
|
|
|
insightData.viewBox = viewBox;
|
|
|
|
}
|
|
|
|
|
|
|
|
let elementBox = insightData.elementBox;
|
|
|
|
|
2025-09-06 11:33:04 +00:00
|
|
|
// console.log( elementBox, insightData, e );
|
2025-03-25 06:42:27 +00:00
|
|
|
|
|
|
|
Box2.updatefromClientRect( insightData.elementBox, e.getBoundingClientRect() );
|
|
|
|
|
|
|
|
if ( Insight.maxHeight.in( e ) )
|
|
|
|
{
|
|
|
|
let maxHeight = Insight.maxHeight.from( e );
|
|
|
|
let isPercentage = /\%/.test( maxHeight );
|
|
|
|
let maxHeightPixel = parseFloat( maxHeight.replace( /\%|px/, "" ) );
|
|
|
|
|
|
|
|
if ( isPercentage )
|
|
|
|
{
|
|
|
|
maxHeightPixel *= window.innerHeight / 100;
|
|
|
|
}
|
|
|
|
|
|
|
|
let size = elementBox.size;
|
|
|
|
|
|
|
|
if ( size.y > maxHeightPixel )
|
|
|
|
{
|
|
|
|
let center = elementBox.center;
|
|
|
|
let offset = maxHeightPixel / 2;
|
|
|
|
elementBox.min.y = center.y - offset;
|
|
|
|
elementBox.max.y = center.y + offset;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
let insight = viewBox.intersectsBox( elementBox );
|
2025-09-06 11:33:04 +00:00
|
|
|
|
|
|
|
// console.log(
|
|
|
|
// "Is in sight", insight,
|
|
|
|
// "DOCUMENT VIEW BOX:", viewBox,
|
|
|
|
// "ELEMENT BOX:", elementBox,
|
|
|
|
// "ELEMENT", e
|
|
|
|
// );
|
2025-03-25 06:42:27 +00:00
|
|
|
|
|
|
|
if ( insight === insightData.isInsight )
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let timeElapsedSinceLastChange = updateTime - insightData.changeTime;
|
|
|
|
|
|
|
|
|
|
|
|
if ( timeElapsedSinceLastChange <= Insight.changeFrequencyMS )
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
insightData.changeTime = updateTime;
|
|
|
|
insightData.isInsight = insight;
|
|
|
|
|
|
|
|
if ( insight )
|
|
|
|
{
|
|
|
|
Insight.in_sight.setAs( e, true );
|
|
|
|
Insight.above_sight.setAs( e, false);
|
|
|
|
Insight.below_sight.setAs( e, false );
|
|
|
|
|
|
|
|
if ( this._insightCallbackElements.has( e ) )
|
|
|
|
{
|
|
|
|
this._insightCallbackElements.get( e ).forEach( c => c() );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2025-09-06 11:33:04 +00:00
|
|
|
|
2025-03-25 06:42:27 +00:00
|
|
|
let isAbove = elementBox.max.y < viewBox.min.y;
|
|
|
|
Insight.in_sight.setAs( e, false );
|
|
|
|
Insight.above_sight.setAs( e, isAbove);
|
|
|
|
Insight.below_sight.setAs( e, ! isAbove );
|
|
|
|
|
|
|
|
if ( this._offsightCallbackElements.has( e ) )
|
|
|
|
{
|
|
|
|
this._offsightCallbackElements.get( e ).forEach( c => c() );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ( insightData.wasInsight )
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setTimeout( ()=>{ Insight.wasSeenFlag.set( e ); }, 200 );
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private _updateViewBox()
|
|
|
|
{
|
|
|
|
if ( ! this._elements )
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( ! this._needsViewBoxUpdate() )
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._hasScreenUpdate = true;
|
|
|
|
|
|
|
|
this._viewBoxSize.x = window.innerWidth;
|
|
|
|
this._viewBoxSize.y = window.innerHeight;
|
|
|
|
|
|
|
|
this._elements.forEach(
|
|
|
|
e =>
|
|
|
|
{
|
|
|
|
let insightElement = InsightElement.ensure( e );
|
|
|
|
let data = insightElement._insightData;
|
|
|
|
data.viewBox = this._getViewBox( e );;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private _needsViewBoxUpdate()
|
|
|
|
{
|
|
|
|
if ( this._viewBoxSize == null )
|
|
|
|
{
|
|
|
|
this._viewBoxSize = new Vector2( 0, 0 );
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( this._viewBoxSize.x !== window.innerWidth || this._viewBoxSize.y !== window.innerHeight )
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private _getViewBox( e:Element ):Box2
|
|
|
|
{
|
|
|
|
|
|
|
|
let relativeStart = 15;
|
|
|
|
let relativeEnd = 100 - relativeStart;
|
|
|
|
|
|
|
|
let coords = Insight.attribute.from( e );
|
|
|
|
|
|
|
|
if ( coords && coords.length > 0 )
|
|
|
|
{
|
|
|
|
let numbers = coords.split( "," );
|
|
|
|
|
|
|
|
if ( numbers.length == 1 )
|
|
|
|
{
|
|
|
|
relativeStart = parseFloat( numbers[ 0 ] );
|
|
|
|
relativeEnd = 100 - relativeStart;
|
|
|
|
}
|
|
|
|
else if ( numbers.length == 2 )
|
|
|
|
{
|
|
|
|
relativeStart = parseFloat( numbers[ 0 ] );
|
|
|
|
relativeEnd = 100 - parseFloat( numbers[ 1 ] );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( isNaN( relativeStart ) )
|
|
|
|
{
|
|
|
|
relativeStart = 15;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( isNaN( relativeEnd ) )
|
|
|
|
{
|
|
|
|
relativeEnd = 100 - relativeStart;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//console.log( "VIEW BOX START/END", relativeStart, relativeEnd );
|
|
|
|
|
|
|
|
let startY = window.innerHeight * relativeStart / 100;
|
|
|
|
let endY = window.innerHeight * relativeEnd / 100;
|
|
|
|
|
2025-09-06 11:33:04 +00:00
|
|
|
return Box2.create( new Vector2( -10000, startY ), new Vector2( 10000, endY ) );
|
2025-03-25 06:42:27 +00:00
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|