mirror of
https://github.com/microsoft/vscode-extension-samples.git
synced 2026-04-27 16:55:44 +08:00
267 lines
6.9 KiB
JavaScript
267 lines
6.9 KiB
JavaScript
// @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' });
|
|
}());
|