Skip to content

Instantly share code, notes, and snippets.

@lxchurbakov
Last active January 25, 2023 09:38
Show Gist options
  • Save lxchurbakov/9d2d936202da7ea4ba7f396b4ef58e50 to your computer and use it in GitHub Desktop.
Save lxchurbakov/9d2d936202da7ea4ba7f396b4ef58e50 to your computer and use it in GitHub Desktop.
plugins
export type Image = HTMLImageElement;
const loadImage = async (url: string) => new Promise<Image>((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (e) => reject(e);
img.src = url;
});
export default class AssetsStore {
private store = new Map<string, Image>();
private status = { loaded: 0, all: 0 };
public add = async (name: string, url: string) => {
this.status.all++;
const img = await loadImage(url);
this.store.set(name, img);
this.status.loaded++;
};
public get = (name: string) => {
return this.store.get(name) || null;
};
};
import { EventEmitter } from '../../libs/events';
export type Point = { x: number, y: number };
export type Rect = { width: number, height: number };
export default class Canvas {
public rect: Rect;
public canvas: HTMLCanvasElement;
public context: CanvasRenderingContext2D;
public onRender = new EventEmitter<CanvasRenderingContext2D>();
constructor (public root: HTMLElement) {
const pixelRatio = window.devicePixelRatio || 1;
this.rect = this.root.getBoundingClientRect();
this.canvas = document.createElement('canvas');
this.canvas.style.width = this.rect.width + 'px';
this.canvas.style.height = this.rect.height + 'px';
this.canvas.width = this.rect.width * pixelRatio;
this.canvas.height = this.rect.height * pixelRatio;
this.root.appendChild(this.canvas);
this.context = this.canvas.getContext('2d');
this.context.scale(pixelRatio, pixelRatio);
requestAnimationFrame(this.render);
}
public render = () => {
this.context.clearRect(0, 0, this.rect.width, this.rect.height);
this.onRender.emitSync(this.context);
requestAnimationFrame(this.render);
};
};
import _ from 'lodash';
type AsyncListener<T> = (arg: T) => Promise<any>;
type SyncListener<T> = (arg: T) => any;
type Listener<T> = AsyncListener<T> | SyncListener<T>;
export class EventEmitter<T> {
listeners: { listener: Listener<T>, priority: number }[] = [];
subscribe = (listener: Listener<T>, priority: number = 0) => {
this.listeners.push({ listener ,priority });
};
unsubscribe = (listener: Listener<T>) => {
this.listeners = _.remove(this.listeners, (l) => l.listener === listener);
};
get = () => this.listeners.sort((a, b) => a.priority - b.priority).map((l) => l.listener);
emitSequenceAsync = (data: T) => {
return this.get().reduce((acc, listener) =>
acc.then((intermediate) => Promise.resolve(listener(intermediate)))
, Promise.resolve(data));
};
emitSequenceSync = (data: T) => {
return this.get().reduce((acc, listener) => listener(acc) as T, data); // unsafe here
};
emitAsync = (data: T) => {
return this.get().reduce((acc, listener) =>
acc.then(() => Promise.resolve(listener(data)))
, Promise.resolve());
};
emitSync = (data: T) => {
this.get().forEach((listener) => {
listener(data);
});
};
emitParallelAsync = (data: T) => {
return Promise.all(this.get().map((listener) => listener(data)));
};
emitParallelSync = (data: T) => {
return this.get().map((listener) => listener(data));
};
};
import { EventEmitter } from '../../libs/events';
import { Point, Rect } from './canvas';
/**
* This plugin handles html events and forwards them
* into "drag", "click" etc
*/
export default class AdvancedEvents {
public onMouseDown = new EventEmitter<Point>();
public onMouseUp = new EventEmitter<Point>();
public onMouseMove = new EventEmitter<Point>();
public onDrag = new EventEmitter<Point>();
public onClick = new EventEmitter<Point>();
public onZoom = new EventEmitter<Point>();
public onKey = new EventEmitter<number>();
public onKeyDown = new EventEmitter<number>();
public onKeyUp = new EventEmitter<number>();
public onSetup = new EventEmitter<HTMLElement>();
private lastmousepos: Point | null = null;
private mousepressed = false;
private rect: DOMRect;
private map = (p: Point) => {
return ({ x: p.x - this.rect.x, y: p.y - this.rect.y });
}
constructor(private node: HTMLElement) {
const eventHandlerNode = document.createElement('div');
eventHandlerNode.style.position = 'absolute';
eventHandlerNode.style.top = '0px';
eventHandlerNode.style.left = '0px';
eventHandlerNode.style.width = '100%';
eventHandlerNode.style.height = '100%';
eventHandlerNode.style.zIndex = '100';
this.node.appendChild(eventHandlerNode);
this.rect = this.node.getBoundingClientRect();
setInterval(() => {
this.rect = this.node.getBoundingClientRect();
}, 1000);
eventHandlerNode.addEventListener('mousedown', (e) => {
const { clientX: x, clientY: y } = e;
this.lastmousepos = { x, y };
this.mousepressed = true;
this.onMouseDown.emitSync(this.map({ x, y }));
});
eventHandlerNode.addEventListener('mousemove', (e) => {
const { clientX: x, clientY: y } = e;
if (this.mousepressed) {
const offsetx = x - this.lastmousepos.x, offsety = y - this.lastmousepos.y;
this.onDrag.emitSync({ x: offsetx, y: offsety });
this.lastmousepos = { x, y };
}
this.onMouseMove.emitSync(this.map({ x, y }));
});
eventHandlerNode.addEventListener('mouseup', (e) => {
const { clientX: x, clientY: y } = e;
if (this.mousepressed) {
const offsetx = this.lastmousepos.x - x, offsety = this.lastmousepos.y - y;
if (Math.abs(offsetx) + Math.abs(offsety) < 20) {
this.onClick.emitSync(this.map({ x, y }));
}
this.mousepressed = false;
};
this.onMouseUp.emitSync(this.map({ x, y }));
});
eventHandlerNode.addEventListener('mousewheel', (e: any) => {
this.onZoom.emitSync({ x: e.deltaX, y: e.deltaY });
e.preventDefault();
});
let keys = {};
document.addEventListener('keydown', (e: any) => {
if (!keys[e.keyCode]) {
this.onKey.emitSync(e.keyCode);
this.onKeyDown.emitSync(e.keyCode);
keys[e.keyCode] = true;
}
});
document.addEventListener('keyup', (e: any) => {
this.onKeyUp.emitSync(e.keyCode);
delete keys[e.keyCode];
});
setTimeout(() => {
this.onSetup.emitSync(eventHandlerNode);
}, 0);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment