class Crusher { hash(s: any): number { return String(s).split("").reduce(function(a, b) { a = ((a << 5) - a) + b.charCodeAt(0); return a & a }, 0); } isDOMElement(o:any): boolean { return ( typeof HTMLElement === "object" ? o instanceof HTMLElement : o && typeof o === "object" && true && o.nodeType === 1 && typeof o.nodeName==="string" ); } } enum BlockType { ONE = 1, TWO = 2, THREE = 3, } class Block { canvas: HTMLCanvasElement; context: CanvasRenderingContext2D; type: BlockType; primary: string; accent: string; cellSize: number; margin: number; scale: number; pos: Position; hash: number; constructor( c: HTMLCanvasElement, ctx: CanvasRenderingContext2D, type: BlockType, primary: string, accent: string, hash: number, cellSize: number, margin: number, scale: number, pos: Position, ) { this.canvas = c; this.context = ctx; this.type = type; this.primary = primary; this.accent = accent; this.hash = hash; this.cellSize = cellSize; this.margin = margin; this.scale = scale; this.pos = pos; } offset() { this.context.save(); this.context.translate(0.6 * this.scale, -0.6 * this.scale); } resetOffset() { this.context.restore(); } makePath(hash: number, offset: number) { const mod = Math.abs(hash + offset) % 4; switch(mod) { case 0: // top this.context.beginPath(); this.context.moveTo(this.pos.x, this.pos.y); this.context.lineTo(this.pos.x + this.cellSize, this.pos.y); this.context.lineTo(this.pos.x + this.cellSize, this.pos.y + this.cellSize); this.context.closePath(); break; case 1: // right this.context.beginPath(); this.context.moveTo(this.pos.x + this.cellSize, this.pos.y); this.context.lineTo(this.pos.x + this.cellSize, this.pos.y + this.cellSize); this.context.lineTo(this.pos.x, this.pos.y + this.cellSize); this.context.closePath(); break; case 2: // bottom this.context.beginPath(); this.context.moveTo(this.pos.x, this.pos.y); this.context.lineTo(this.pos.x, this.pos.y + this.cellSize); this.context.lineTo(this.pos.x + this.cellSize, this.pos.y + this.cellSize); this.context.closePath(); break; case 3: // left this.context.beginPath(); this.context.moveTo(this.pos.x, this.pos.y); this.context.lineTo(this.pos.x + this.cellSize, this.pos.y); this.context.lineTo(this.pos.x, this.pos.y + this.cellSize); this.context.closePath(); break; default: // top this.context.beginPath(); this.context.moveTo(this.pos.x, this.pos.y); this.context.lineTo(this.pos.x + this.cellSize, this.pos.y); this.context.lineTo(this.pos.x, this.pos.y + this.cellSize); this.context.closePath(); } } draw() { this.offset(); if (this.type === BlockType.ONE) { this.makePath(this.hash, this.hash % 3); this.context.fillStyle = this.primary; this.context.fill(); this.makePath(this.hash, this.hash % 5); this.context.fillStyle = this.accent; this.context.fill(); } else if (this.type === BlockType.TWO) { this.makePath(this.hash, this.hash % 4); this.context.fillStyle = this.accent; this.context.fill(); this.makePath(this.hash, this.hash % 3); this.context.fillStyle = this.primary; this.context.fill(); } else { this.makePath(this.hash, this.hash % 7); this.context.fillStyle = this.accent; this.context.fill(); this.makePath(this.hash, this.hash % 8); this.context.fillStyle = this.primary; this.context.fill(); } this.resetOffset(); } } class Position { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } } class Shape { canvas: HTMLCanvasElement; context: CanvasRenderingContext2D; hash: number; primary: string; accent: string; pos: Position; scale: number; cellSize: number; strokeColor: string; constructor( c: HTMLCanvasElement, ctx: CanvasRenderingContext2D, hash: number, primary: string, accent: string, pos: Position, scale: number, cellSize: number, strokeColor: string ) { this.canvas = c; this.context = ctx; this.hash = hash; this.primary = primary; this.accent = accent; this.pos = pos; this.scale = scale; this.cellSize = cellSize; this.strokeColor = strokeColor; } getColor() { return [this.primary, this.accent][Math.abs(this.hash % 2)]; } makePath() { const mod = Math.abs(this.hash + 1) % 4; switch(mod) { case 0: // square this.context.beginPath(); this.context.moveTo(this.pos.x, this.pos.y); this.context.lineTo(this.pos.x + (this.cellSize / 2), this.pos.y); this.context.lineTo(this.pos.x + (this.cellSize / 2), this.pos.y - (this.cellSize / 2)); this.context.lineTo(this.pos.x, this.pos.y - (this.cellSize / 2)); this.context.closePath(); break; case 1: //circle this.context.beginPath(); this.context.arc( this.pos.x + (this.cellSize / Math.PI) - 5, this.pos.y - (this.cellSize / Math.PI) + 5, this.cellSize / 3, 0, Math.PI * 2, true ); break; case 2: // triangle this.context.beginPath(); this.context.moveTo(this.pos.x, this.pos.y); this.context.lineTo(this.pos.x + (this.cellSize * 0.65), this.pos.y); this.context.lineTo(this.pos.x, this.pos.y - (this.cellSize * 0.65)); this.context.closePath(); break; case 3: // oval this.context.beginPath(); this.context.moveTo(this.pos.x - (this.cellSize * 0.2), this.pos.y + (this.cellSize * 0.2)); this.context.quadraticCurveTo(this.pos.x + (this.cellSize * 0.4), this.pos.y, this.pos.x + (this.cellSize * 0.5), this.pos.y - (this.cellSize * 0.5)); this.context.moveTo(this.pos.x + (this.cellSize * 0.5), this.pos.y - (this.cellSize * 0.5)); this.context.quadraticCurveTo(this.pos.x , this.pos.y - (this.cellSize * 0.4), this.pos.x - (this.cellSize * 0.2), this.pos.y + (this.cellSize * 0.2)); break; default: //square this.context.beginPath(); this.context.moveTo(this.pos.x, this.pos.y); this.context.lineTo(this.pos.x + (this.cellSize / 2), this.pos.y); this.context.lineTo(this.pos.x + (this.cellSize / 2), this.pos.y - (this.cellSize / 2)); this.context.lineTo(this.pos.x, this.pos.y - (this.cellSize / 2)); this.context.closePath(); } } draw(hasStroke: boolean, strokeWeight: number) { const color = this.getColor(); this.context.globalCompositeOperation = "source-over"; this.makePath(); this.context.fillStyle = color; this.context.strokeStyle = this.strokeColor; this.context.lineWidth = this.scale * ((4/5 * strokeWeight) / this.canvas.width); this.context.lineJoin = "round"; this.context.lineCap = "round"; this.context.fill(); if (hasStroke) { this.context.stroke(); } } } /** * @name IdentiHeart * @author Schlipak * @copyright Apache license 2015 Guillaume de Matos */ export class Identiheart { canvas: HTMLCanvasElement context: CanvasRenderingContext2D margin: number crusher: Crusher palette = [ '#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800', '#FF5722', '#795548', '#607D8B' ]; primary: string = "#F44336"; accent: string = "#E91E63"; scale: number; cellSize: number; hash: number = 0; blocks: Block[] = []; shape?: Shape; hasStroke: boolean = true; strokeWeight = 500; strokeColor = "#000000"; compositeOperation: GlobalCompositeOperation = 'multiply'; constructor(c: HTMLCanvasElement, margin?: number, scale?: number, ctx?: CanvasRenderingContext2D) { this.canvas = c; this.context = ctx ?? c.getContext("2d")!; this.margin = margin || 5; this.scale = scale || 20; this.crusher = new Crusher(); this.cellSize = (this.canvas.width / 2) - (this.margin * this.scale); } setUsername(username: string) { this.hash = this.crusher.hash(username); return this; } setPalette(palette:string[]) { if (palette.length < 2) { throw new Error('Palette must be more than two elements long'); } this.palette = palette; return this; } setHasStroke(b: boolean) { this.hasStroke = b; return this; } setStrokeWeight(weight: number) { this.strokeWeight = weight; return this; } setStrokeColor(color: string) { this.strokeColor = color; return this; } setCompositeOperation(operation: GlobalCompositeOperation) { this.compositeOperation = operation; return this; } setCanvas(canvas: HTMLCanvasElement) { this.canvas = canvas; this.context = canvas.getContext('2d')!; return this; } init() { // Purge the block array this.blocks = new Array(); // Purge the shape this.shape = undefined; // Generate colors const crusher = new Crusher(); const subHash = crusher.hash(this.hash); this.accent = this.palette[Math.abs(subHash % this.palette.length)]; // Clear the canvas this.context.globalCompositeOperation = "source-over"; this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); this.context.globalCompositeOperation = this.compositeOperation; return this; } draw() { this.init(); // Rotate the canvas -45deg this.context.save(); this.context.translate(this.canvas.width/2, this.canvas.height/2); this.context.rotate(- Math.PI / 4); this.context.translate(-this.canvas.width/2, -this.canvas.height/2); this.generateBlocks(); this.drawBlocks(); if (this.hasStroke) { this.drawOutline(); } this.shape = new Shape(this.canvas, this.context, this.hash, this.primary, this.accent, { x: (this.margin * this.scale) + 1.5 * this.cellSize, y: (this.margin * this.scale) + 0.5 * this.cellSize }, this.scale, this.cellSize, this.strokeColor); this.shape.draw(this.hasStroke, this.strokeWeight); // Restore the original matrix this.context.restore(); return this; } offset() { this.context.save(); this.context.translate(0.6 * this.scale, - 0.6 * this.scale); } resetOffset() { this.context.restore(); } drawOutline() { this.offset(); this.context.globalCompositeOperation = "source-over"; // Outer lines this.context.beginPath(); this.context.moveTo(this.margin * this.scale, this.margin * this.scale); this.context.lineTo(this.margin * this.scale, this.canvas.height - (this.margin * this.scale)); this.context.lineTo(this.canvas.width - (this.margin * this.scale), this.canvas.height - (this.margin * this.scale)); this.context.lineTo(this.canvas.width - (this.margin * this.scale), this.canvas.height / 2); this.context.lineTo(this.canvas.width / 2, this.canvas.height / 2); this.context.lineTo(this.canvas.width / 2, this.margin * this.scale); this.context.closePath(); this.context.strokeStyle = this.strokeColor; this.context.lineWidth = this.scale * (this.strokeWeight / this.canvas.width); this.context.lineJoin = "round"; this.context.lineCap = "round"; this.context.stroke(); // Inner lines this.context.beginPath(); this.context.moveTo(this.canvas.width / 2, this.canvas.height / 2); this.context.lineTo(this.margin * this.scale, this.canvas.height / 2); this.context.moveTo(this.canvas.width / 2, this.canvas.height / 2); this.context.lineTo(this.canvas.width / 2, this.canvas.height - (this.margin * this.scale)); this.context.stroke(); this.resetOffset(); this.context.globalCompositeOperation = this.compositeOperation; } generateBlocks() { /* c: HTMLCanvasElement, ctx: CanvasRenderingContext2D, type: BlockType, primary: string, accent: string, hash: number, cellSize: number, margin: number, scale: number, pos: Position, * */ let b1 = new Block( this.canvas, this.context, BlockType.ONE, this.primary, this.accent, this.hash, this.cellSize, this.margin, this.scale, new Position(this.margin * this.scale,this.margin * this.scale)); this.blocks.push(b1); let b2 = new Block( this.canvas, this.context, BlockType.TWO, this.primary, this.accent, this.hash, this.cellSize, this.margin, this.scale, new Position(this.margin * this.scale,this.canvas.height / 2) ); this.blocks.push(b2); let b3 = new Block( this.canvas, this.context, BlockType.THREE, this.primary, this.accent, this.hash, this.cellSize, this.margin, this.scale, new Position(this.canvas.width / 2, this.canvas.height / 2) ); this.blocks.push(b3); } drawBlocks() { if (this.blocks.length == 0) { return false; } for (var i = 0; i < this.blocks.length; i++) { this.blocks[i].draw(); } } }