UNPKG

flyde-vscode

Version:

Flyde is an open-source, visual programming language. It runs in the IDE, integrates with existing TypeScript code, both browser and Node.js.

267 lines (225 loc) 7.06 kB
// @ts-check // This script is run within the webview itself (function () { // @ts-ignore const vscode = acquireVsCodeApi(); /** * A drawn line. */ class Stroke { constructor(/** @type {string} */ color, /** @type {Array<[number, number]> | undefined} */ stroke) { this.color = color; /** @type {Array<[number, number]>} */ this.stroke = stroke || []; } addPoint(/** @type {number} */ x, /** @type {number} */ y) { this.stroke.push([x, y]) } } /** * @param {Uint8Array} initialContent * @return {Promise<HTMLImageElement>} */ async function loadImageFromData(initialContent) { const blob = new Blob([initialContent], { 'type': 'image/png' }); const url = URL.createObjectURL(blob); try { const img = document.createElement('img'); img.crossOrigin = 'anonymous'; img.src = url; await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; }); return img; } finally { URL.revokeObjectURL(url); } } class PawDrawEditor { constructor( /** @type {HTMLElement} */ parent) { this.ready = false; this.editable = false; this.drawingColor = 'black'; /** @type {Array<Stroke>} */ this.strokes = []; /** @type {Stroke | undefined} */ this.currentStroke = undefined; this._initElements(parent); } addPoint(/** @type {number} */ x, /** @type {number} */ y) { if (this.currentStroke) { this.currentStroke.addPoint(x, y) } } beginStoke(/** @type {string} */ color) { this.currentStroke = new Stroke(color); this.strokes.push(this.currentStroke); } endStroke() { const previous = this.currentStroke; this.currentStroke = undefined; return previous; } setEditable(editable) { this.editable = editable; const colorButtons = /** @type {NodeListOf<HTMLButtonElement>} */ (document.querySelectorAll('.drawing-controls button')); for (const colorButton of colorButtons) { colorButton.disabled = !editable; } } _initElements(/** @type {HTMLElement} */ parent) { const colorButtons = /** @type {NodeListOf<HTMLButtonElement>} */ (document.querySelectorAll('.drawing-controls button')); for (const colorButton of colorButtons) { colorButton.addEventListener('click', e => { e.stopPropagation(); colorButtons.forEach(button => button.classList.remove('active')); colorButton.classList.add('active'); this.drawingColor = colorButton.dataset['color']; }); } this.wrapper = document.createElement('div'); this.wrapper.style.position = 'relative'; parent.append(this.wrapper); this.initialCanvas = document.createElement('canvas'); this.initialCtx = this.initialCanvas.getContext('2d'); this.wrapper.append(this.initialCanvas); this.drawingCanvas = document.createElement('canvas'); this.drawingCanvas.style.position = 'absolute'; this.drawingCanvas.style.top = '0'; this.drawingCanvas.style.left = '0'; this.drawingCtx = this.drawingCanvas.getContext('2d'); this.wrapper.append(this.drawingCanvas); let isDrawing = false; parent.addEventListener('mousedown', () => { if (!this.ready || !this.editable) { return; } this.beginStoke(this.drawingColor); this.drawingCtx.strokeStyle = this.drawingColor; isDrawing = true; document.body.classList.add('isDrawing'); this.drawingCtx.beginPath(); }); document.body.addEventListener('mouseup', async () => { if (!isDrawing || !this.ready || !this.editable) { return; } isDrawing = false; document.body.classList.remove('isDrawing'); this.drawingCtx.closePath(); const edit = this.endStroke(); if (edit.stroke.length) { vscode.postMessage({ type: 'stroke', color: edit.color, stroke: edit.stroke, }); } }); parent.addEventListener('mousemove', e => { if (!isDrawing || !this.ready || !this.editable) { return; } const rect = this.wrapper.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; this.drawingCtx.lineTo(x, y); this.drawingCtx.stroke(); this.addPoint(x, y); }); } _redraw() { this.drawingCtx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height); for (const stroke of this.strokes) { this.drawingCtx.strokeStyle = stroke.color; this.drawingCtx.beginPath(); for (const [x, y] of stroke.stroke) { this.drawingCtx.lineTo(x, y); } this.drawingCtx.stroke(); this.drawingCtx.closePath(); } } /** * @param {Uint8Array | undefined} data * @param {Array<Stroke> | undefined} strokes */ async reset(data, strokes = []) { if (data) { const img = await loadImageFromData(data); this.initialCanvas.width = this.drawingCanvas.width = img.naturalWidth; this.initialCanvas.height = this.drawingCanvas.height = img.naturalHeight; this.initialCtx.drawImage(img, 0, 0); this.ready = true; } this.strokes = strokes; this._redraw(); } /** * @param {Array<Stroke> | undefined} strokes */ async resetUntitled(strokes = []) { const size = 100; this.initialCanvas.width = this.drawingCanvas.width = size; this.initialCanvas.height = this.drawingCanvas.height = size; this.initialCtx.save(); { this.initialCtx.fillStyle = 'white'; this.initialCtx.fillRect(0, 0, size, size); } this.initialCtx.restore(); this.ready = true; this.strokes = strokes; this._redraw(); } /** @return {Promise<Uint8Array>} */ async getImageData() { const outCanvas = document.createElement('canvas'); outCanvas.width = this.drawingCanvas.width; outCanvas.height = this.drawingCanvas.height; const outCtx = outCanvas.getContext('2d'); outCtx.drawImage(this.initialCanvas, 0, 0); outCtx.drawImage(this.drawingCanvas, 0, 0); const blob = await new Promise(resolve => { outCanvas.toBlob(resolve, 'image/png') }); return new Uint8Array(await blob.arrayBuffer()); } } const editor = new PawDrawEditor(document.querySelector('.drawing-canvas')); // Handle messages from the extension window.addEventListener('message', async e => { const { type, body, requestId } = e.data; switch (type) { case 'init': { editor.setEditable(body.editable); if (body.untitled) { await editor.resetUntitled(); return; } else { // Load the initial image into the canvas. await editor.reset(body.value); return; } } case 'update': { const strokes = body.edits.map(edit => new Stroke(edit.color, edit.stroke)); await editor.reset(body.content, strokes) return; } case 'getFileData': { // Get the image data for the canvas and post it back to the extension. editor.getImageData().then(data => { vscode.postMessage({ type: 'response', requestId, body: Array.from(data) }); }); return; } } }); // Signal to VS Code that the webview is initialized. vscode.postMessage({ type: 'ready' }); }());