timeplayer
Version:
A time player/travel module
369 lines (321 loc) • 11.7 kB
text/typescript
import Control from './control';
interface TimeplyerOptions {
width?: number,
height?: number,
theme?: 'dark',
colors: { [key: string]: string },
dates: string[],
}
enum Events {
change = 'change'
}
const Config = {
minHeight: 40,
minWidth: 100,
padding: 8,
pointSpace: 10,
defaultColors: {
scaleStart: '#000',
scaleEnd: '#000',
scaleLine: '#000',
scaleTextStart: '#333',
scaleTextEnd: '#333',
scalePointsSmall: '#0063a9',
scalePointsBig: '#0190f5',
activePointStroke: 'black',
activePointFill: 'rgba(100, 191, 255, 1.0)',
activeTipsBackground: 'rgba(100, 191, 255, 0.8)',
activeTipsText: 'white',
hoverPointStroke: 'black',
hoverPointFill: 'white',
hoverTipsBackground: 'rgba(0, 0, 0, 0.8)',
hoverTipsText: 'white',
controlBtn: '#000',
},
darkColors: {
scaleStart: '#bbb',
scaleEnd: '#bbb',
scaleLine: '#bbb',
scaleTextStart: '#ccc',
scaleTextEnd: '#ccc',
scalePointsSmall: '#fff',
scalePointsBig: '#fff',
activePointStroke: '#fff',
activePointFill: 'rgba(100, 191, 255, 1.0)',
activeTipsBackground: 'rgba(100, 191, 255, 0.8)',
activeTipsText: 'white',
hoverPointStroke: '#fff',
hoverPointFill: '#eee',
hoverTipsBackground: '#666',
hoverTipsText: 'white',
controlBtn: '#fff',
},
}
class Timeplyer {
private dom: HTMLElement; // the main dom where we put the component
private height: number; // the height of the dom
private width: number; // the width of the dom
private canvas: HTMLCanvasElement; // the main canvas element
private ctx: CanvasRenderingContext2D; // the context of the canvas
private dates: string[]; // the dates we want to show
private _activeIndex: number; // the indexof active data ;
private hoverIndex: number | null; // the index
private Config: { [key: string]: any };
private colors: {
scaleStart: string;
scaleEnd: string;
scaleLine: string;
scaleTextStart: string;
scaleTextEnd: string;
scalePointsSmall: string;
scalePointsBig: string;
activePointStroke: string;
activePointFill: string;
activeTipsBackground: string;
activeTipsText: string;
hoverPointStroke: string;
hoverPointFill: string;
hoverTipsBackground: string;
hoverTipsText: string;
controlBtn: string;
};
private padding: number; // the padding of the right and left side
private events: {
[key in Events]?: Function[];
}
private control: Control;
constructor(dom: HTMLElement, options: TimeplyerOptions) {
this.dom = dom;
this.dates = options.dates;
this.Config = Object.assign({}, Config, options);
this._activeIndex = this.dates.length - 1;
const domStyle = getComputedStyle(this.dom);
this.padding = this.Config.padding;
this.height = options.height || Math.max(parseFloat(domStyle.height), this.Config.minHeight);
this.width = options.width || Math.max(parseFloat(domStyle.width), this.Config.minWidth) - this.padding * 2;
// them
if (options.colors) {
this.colors = Object.assign({}, this.Config.defaultColors, options.colors);
} else {
this.colors = options.theme === 'dark' ? this.Config.darkColors : this.Config.defaultColors;
}
// set upcontrol
this.setupControl();
// create canvas
this.setupCanvas();
this.setupEvens();
// draw everything
this.draw();
}
private setupControl() {
const { controlBtn } = this.colors;
this.control = new Control({
valueMax: this.dates.length - 1,
value: this._activeIndex,
colors: { controlBtn },
onChange: (value: number) => {
this.activeIndex = value;
this.draw();
},
});
this.dom.appendChild(this.control.dom);
}
private setupCanvas() {
const { padding, height, dom } = this;
const width = this.width + padding * 2;
// create canvas
const canvas = this.canvas = document.createElement('canvas');
const ctx = this.ctx = canvas.getContext('2d');
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
dom.appendChild(canvas);
// scale canvas
var dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
}
private setupEvens() {
const { canvas, padding, width, dates } = this;
this.events = {};
canvas.onmousemove = (e: MouseEvent) => {
const offset = e.offsetX - padding;
const precent = offset / width;
let index = Math.round(precent * (dates.length - 1));
index = Math.max(0, index);
index = Math.min(dates.length - 1, index);
this.hoverIndex = index;
this.draw();
};
canvas.onmouseleave = () => {
this.hoverIndex = null;
this.draw();
};
canvas.onclick = (e: MouseEvent) => {
const offset = e.offsetX - padding;
const precent = offset / width;
let index = Math.round(precent * (dates.length - 1));
index = Math.max(0, index);
index = Math.min(dates.length, index);
this.activeIndex = index;
this.control.setActive(index);
this.draw();
};
}
private draw() {
const { ctx } = this;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
this.drawScale();
this.drawHover();
this.drawActive();
}
private drawScale() {
const { ctx, padding, width, height, dates, colors, Config } = this;
const step = width / (dates.length - 1);
// draw left and right end line
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding, 0);
ctx.lineTo(padding, height);
ctx.strokeStyle = colors.scaleStart;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(width + padding, 0);
ctx.lineTo(width + padding, height);
ctx.strokeStyle = colors.scaleEnd;
ctx.stroke();
// draw horizontal line
ctx.beginPath();
ctx.moveTo(padding, height / 2);
ctx.lineTo(width + padding, height / 2);
ctx.lineWidth = 1;
ctx.strokeStyle = colors.scaleLine;
ctx.stroke();
// draw start and end text
ctx.textBaseline = 'top';
ctx.font = "10px Orbitron";
ctx.textAlign = 'left';
ctx.fillStyle = colors.scaleTextStart;
ctx.fillText(dates[0], padding + 4, 0);
ctx.textAlign = 'right';
ctx.fillStyle = colors.scaleTextEnd;
ctx.fillText(dates[dates.length - 1], width + padding - 4, 0);
// draw scale points
ctx.beginPath();
let stepWidth = 0;
const bigPoints = [];
for (let i = 1; i * step < width; i++) {
stepWidth += step;
let fillcolor;
let pointWidth;
if (stepWidth >= Config.pointSpace) {
stepWidth = 0;
pointWidth = 4;
bigPoints.push({
leftX: i * step - pointWidth / 2 + padding,
leftY: height / 2 - pointWidth / 2,
});
} else {
pointWidth = 1.5;
fillcolor = colors.scalePointsSmall;
const leftX = i * step - pointWidth / 2 + padding;
const leftY = height / 2 - pointWidth / 2;
ctx.fillStyle = fillcolor;
ctx.fillRect(leftX, leftY, pointWidth, pointWidth);
}
}
bigPoints.forEach(({ leftX, leftY }) => {
ctx.fillStyle = colors.scalePointsBig;
ctx.fillRect(leftX, leftY, 4, 4);
})
}
private drawHover() {
const { width, height, ctx, dates, padding, hoverIndex, colors } = this;
if (hoverIndex !== null) {
const step = width / (dates.length - 1);
ctx.beginPath();
ctx.arc(step * hoverIndex + padding, height / 2, 4, 0, Math.PI * 2);
ctx.lineWidth = 1;
ctx.fillStyle = colors.hoverPointFill;
ctx.strokeStyle = colors.hoverPointStroke;
ctx.stroke();
ctx.fill()
ctx.textBaseline = 'top';
const textInfo = ctx.measureText(dates[hoverIndex]);
let bgLeft;
let textX;
const bgOffset = 5;
const textOffset = 5;
if (hoverIndex > dates.length / 2) {
ctx.textAlign = 'right';
bgLeft = step * hoverIndex + padding - textInfo.width - textOffset * 2 - bgOffset;
textX = step * hoverIndex + padding - textOffset - bgOffset;
} else {
ctx.textAlign = 'left';
bgLeft = step * hoverIndex + padding + bgOffset;
textX = bgLeft + textOffset;
}
ctx.fillStyle = colors.hoverTipsBackground;
ctx.fillRect(bgLeft, 0, textInfo.width + 10, 14);
ctx.fillStyle = colors.hoverTipsText;
ctx.fillText(dates[hoverIndex], textX, 2);
}
}
private drawActive() {
const { width, dates, ctx, _activeIndex, height, colors, padding } = this;
const step = width / (dates.length - 1);
ctx.beginPath();
ctx.arc(step * _activeIndex + padding, height / 2, 4, 0, Math.PI * 2);
ctx.lineWidth = 2;
ctx.strokeStyle = colors.activePointStroke;
ctx.fillStyle = colors.activePointFill;
ctx.stroke();
ctx.fill();
ctx.closePath();
//
ctx.font = "12px Orbitron";
ctx.textBaseline = 'bottom';
ctx.textAlign = 'center';
const textInfo = ctx.measureText(dates[_activeIndex]);
let bgLeft;
let textX;
let textPadding = 5;
let bgOffset = 5;
const rectWidth = textInfo.width + textPadding * 2;
if (_activeIndex > dates.length / 2) {
ctx.textAlign = 'right';
bgLeft = step * _activeIndex + padding - rectWidth - bgOffset;
textX = step * _activeIndex + padding - bgOffset - textPadding;
} else {
ctx.textAlign = 'left';
bgLeft = step * _activeIndex + padding + bgOffset;
textX = bgLeft + textPadding;
}
ctx.fillStyle = colors.activeTipsBackground;
ctx.fillRect(bgLeft, height - 14, rectWidth, 14);
//
ctx.fillStyle = colors.activeTipsText;
ctx.fillText(dates[_activeIndex], textX, height);
}
on(type: Events, fn: Function) {
console.log(this.events)
this.events[type] = this.events[type] || [];
this.events[type].push(fn);
}
off(type: Events, fn: Function) {
const index = this.events[type].indexOf(fn);
if (index !== -1) {
this.events[type].splice(index, 1);
}
}
set activeIndex(index: number) {
this._activeIndex = index;
(this.events['change'] || []).forEach(fn => {
fn(index, this.dates[index]);
});
}
get activeIndex(): number {
return this._activeIndex;
}
}
export default Timeplyer;