import {server} from "../../../socket/socketClient"; import * as ui from "../../ui"; import onresize = require('onresize'); import * as utils from "../../../common/utils"; import {CompositeDisposible} from "../../../common/events"; type Editor = monaco.editor.ICodeEditor; type Change = monaco.editor.IModelContentChangedEvent2; export class Blaster { private disposible = new CompositeDisposible(); private detached = false; constructor(private cm: Editor) { this.disposible.add(cm.onDidChangeModelContent(this.handleChange)); this.initCanvas(); this.loop(); } dispose() { this.detached = true; this.disposible.dispose(); } canvas: HTMLCanvasElement | null = null; ctx: CanvasRenderingContext2D | null = null; initCanvas = () => { const cm = this.cm; this.canvas = document.createElement('canvas'); cm.getDomNode().parentElement.appendChild(this.canvas); this.ctx = this.canvas.getContext('2d'); this.canvas.style.position = 'absolute'; this.canvas.style.top = "0"; this.canvas.style.left = "0"; this.canvas.style.bottom = "0"; this.canvas.style.right = "0"; this.canvas.style.zIndex = '1'; this.canvas.style.pointerEvents = 'none'; let measureCanvas = () => { let parent = cm.getDomNode(); this.canvas.width = parent.clientWidth this.canvas.height = parent.clientHeight; } this.disposible.add(onresize.on(measureCanvas)); cm.layout = utils.intercepted({ context: cm, orig: cm.layout, intercept: measureCanvas }); measureCanvas(); } loop = () => { // If unmounted stop if (this.detached) return; // Setup for next loop requestAnimationFrame(this.loop); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.drawParticles(); } particles: Particle[] = []; drawParticles = (timeDelta?: number) => { // return if no particles if (!this.particles.length) return; // animate the particles for (let particle of this.particles) { if (particle.effect === Effect.Add) { this.effect1(particle); } else if (particle.effect === Effect.Delete) { this.effect2(particle); } } // clear out the particles that are no longer relevant post animation this.particles = this.particles.filter(particle => particle && particle.alpha > 0.01 && particle.size > 0.5); } PARTICLE_GRAVITY = 0.08; PARTICLE_ALPHA_FADEOUT = 0.96; effect1(particle: Particle) { particle.vy += this.PARTICLE_GRAVITY; particle.x += particle.vx; particle.y += particle.vy; particle.alpha *= this.PARTICLE_ALPHA_FADEOUT; this.ctx.fillStyle = 'rgba(' + particle.color[0] + ',' + particle.color[1] + ',' + particle.color[2] + ',' + particle.alpha + ')'; this.ctx.fillRect(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, particle.size); } // Effect based on Soulwire's demo: http://codepen.io/soulwire/pen/foktm effect2(particle: Particle) { particle.x += particle.vx; particle.y += particle.vy; particle.vx *= particle.drag; particle.vy *= particle.drag; particle.theta += random(-0.5, 0.5); particle.vx += Math.sin(particle.theta) * 0.1; particle.vy += Math.cos(particle.theta) * 0.1; particle.size *= 0.96; this.ctx.fillStyle = 'rgba(' + particle.color[0] + ',' + particle.color[1] + ',' + particle.color[2] + ',' + particle.alpha + ')'; this.ctx.beginPath(); this.ctx.arc(particle.x - particle.size / 2, particle.y - particle.size / 2, particle.size, 0, 2 * Math.PI); this.ctx.fill(); } // spawn particles PARTICLE_NUM_RANGE = { min: 5, max: 10 }; throttledSpawnParticles = utils.throttle((effect: Effect) => { const editor = this.cm; const cursorPos = editor.getPosition(); /** The position relative to dom node of editor */ const pos = editor.getScrolledVisiblePosition(cursorPos); /** Get the color for the dom token */ const editorNode = editor.getDomNode(); const editorNodeRect = editorNode.getBoundingClientRect(); const posForNode = { x: editorNodeRect.left + pos.left, y: editorNodeRect.top + pos.top, } const node = document.elementFromPoint(posForNode.x - 5, posForNode.y + 5); const color = getRGBComponents(node); // Now create the particles const numParticles = random(this.PARTICLE_NUM_RANGE.min, this.PARTICLE_NUM_RANGE.max); for (var i = 0; i < numParticles; i++) { this.particles.push(this.createParticle(pos.left + 15, pos.top - 5, color, effect)); } }, 100); PARTICLE_VELOCITY_RANGE = { x: [-1, 1], y: [-3.5, -1.5] } createParticle(x: number, y: number, color: [string, string, string], effect: Effect) { var p: Particle = { x: x, y: y + 10, alpha: 1, color: color, effect: effect, // modifed below drag: 0, wander: 0, theta: 0, size: 0, vx: 0, vy: 0, }; if (effect == Effect.Add) { p.size = random(2, 4); p.vx = this.PARTICLE_VELOCITY_RANGE.x[0] + Math.random() * (this.PARTICLE_VELOCITY_RANGE.x[1] - this.PARTICLE_VELOCITY_RANGE.x[0]); p.vy = this.PARTICLE_VELOCITY_RANGE.y[0] + Math.random() * (this.PARTICLE_VELOCITY_RANGE.y[1] - this.PARTICLE_VELOCITY_RANGE.y[0]); } else if (effect == Effect.Delete) { p.size = random(2, 8); p.drag = 0.92; p.vx = random(-3, 3); p.vy = random(-3, 3); p.wander = 0.15; p.theta = random(0, 360) * Math.PI / 180; } return p; } handleChange = (change: Change) => { // setup particles if (change.text) { this.throttledSpawnParticles(Effect.Add); } else { this.throttledSpawnParticles(Effect.Delete); } }; } /** * General Utilities */ enum Effect { Add = 1, Delete = 2, } interface Particle { x: number; y: number; alpha: number; color: [string, string, string]; effect: Effect; // Based on effect drag?: number; wander?: number; theta?: number; size?: number; vx?: number; vy?: number; } /** Get a random number in the min-max range, inclusive */ function random(min: number, max: number) { if (!max) { max = min; min = 0; } return min + ~~(Math.random() * (max - min + 1)) } /** Get the colors of the html node */ function getRGBComponents(node: Element): [string, string, string] { if (node) { try { var color = getComputedStyle(node).color; return color.match(/(\d+), (\d+), (\d+)/).slice(1) as any; } catch (e) { return ['255', '255', '255']; } } else { return ['255', '255', '255']; } } /** * Registering it with monaco */ import CommonEditorRegistry = monaco.CommonEditorRegistry; import ICommonCodeEditor = monaco.ICommonCodeEditor; import TPromise = monaco.Promise; import EditorAction = monaco.EditorAction; import KeyMod = monaco.KeyMod; import KeyCode = monaco.KeyCode; import ServicesAccessor = monaco.ServicesAccessor; import IActionOptions = monaco.IActionOptions; import EditorContextKeys = monaco.EditorContextKeys; class ToggleBlasterAction extends EditorAction { constructor() { super({ id: 'editor.action.toggleBlaster', label: 'Toggle Blaster', alias: 'Toggle Blaster', precondition: EditorContextKeys.Writable, kbOpts: { kbExpr: EditorContextKeys.TextFocus, primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_O } }); } public blaster: Blaster | null = null; public run(accessor:ServicesAccessor, editor:ICommonCodeEditor): void | TPromise { if (!this.blaster) { this.blaster = new Blaster(editor as Editor); ui.notifySuccessNormalDisappear('Have fun 🌹!'); } else { this.blaster.dispose(); this.blaster = null; ui.notifyInfoQuickDisappear('Hope you had fun 💖'); } } } CommonEditorRegistry.registerEditorAction(new ToggleBlasterAction());