Examples
Create a plugin that can copy the area selected by the cursor in the turtle canvas to the clipboard
copy-selected-area.js
var clicked = false;
var mouseDown = false;
var xDown = 0;
var yDown = 0;
var xMove = 0;
var yMove = 0;
var clip = false;
var proccessingImage = false;
var dataURL = '';
var start;
var showCopyMessageTime;
var copyMessage = null;
var copyState = '';
async function copyImgToClipboard(dataUrl) {
try {
const response = await fetch(dataUrl);
const blob = await response.blob();
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
copyMessage = 'Copied successfully!';
copyState = 'ok';
showCopyMessageTime = Date.now();
proccessingImage = false;
} catch (err) {
copyMessage = err.message;
copyState = 'error';
showCopyMessageTime = Date.now();
proccessingImage = false;
}
}
function cutImageFromCanvas(canvas, x, y, width, height, type = 'url') {
return new Promise(resolve => {
const croppedCanvas = document.createElement('canvas');
var ratio = window.devicePixelRatio || 1;
croppedCanvas.width = width * ratio;
croppedCanvas.height = height * ratio;
croppedCanvas.style.width = width + 'px';
croppedCanvas.style.height = height + 'px';
const ctx = croppedCanvas.getContext('2d');
ctx.drawImage(canvas, x * ratio, y * ratio, width * ratio, height * ratio, 0, 0, croppedCanvas.width, croppedCanvas.height);
croppedCanvas.toBlob(blob => {
const reader = new FileReader();
reader.onloadend = () => {
const dataUrl = reader.result;
resolve(dataUrl);
};
reader.readAsDataURL(blob);
});
});
}
function getAnimateData(keyframes, name, time, duration) {
const percent = (time % duration) / duration * 100;
var start = 0;
var end = 0;
Object.keys(keyframes).forEach((keyframe, i) => {
if (Object.keys(keyframes).length > i + 1) {
if (keyframe <= percent && percent <= Object.keys(keyframes)[i + 1]) {
start = +keyframe;
end = +Object.keys(keyframes)[i + 1];
}
}
})
if (!keyframes[start].hasOwnProperty(name) && !keyframes[end].hasOwnProperty) {
return 0;
} else {
return keyframes[start][name] + (keyframes[end][name] - keyframes[start][name]) * (percent - start) / (end - start);
}
}
var tipTextKeyframes = {
0: {
opacity: 0.3
},
50: {
opacity: 1
},
100: {
opacity: 0.3
}
}
const copySelectedArea = {
name: 'copy-selected-area',
init: (api) => {
api.on('mousedown', (e) => {
mouseDown = true;
xDown = e.x;
yDown = e.y;
xMove = e.x;
yMove = e.y;
clicked = true;
copyMessage = null;
copyState = '';
})
api.on('mousemove', (e) => {
xMove = e.x;
yMove = e.y;
})
window.addEventListener('mouseup', (e) => {
if (mouseDown == true) {
clip = true;
proccessingImage = true;
}
mouseDown = false;
})
window.addEventListener('blur', (e) => {
mouseDown = false;
})
start = Date.now();
},
execute: async (api) => {
var text = '';
var animation = false;
if (Date.now() - showCopyMessageTime > 3000) {
copyMessage = null;
copyState = '';
}
if (copyMessage != null) {
text = copyMessage;
} else if (proccessingImage == true) {
text = 'Cutting image and copy to clipboard...';
copyMessage = null;
copyState = '';
} else if (mouseDown == true) {
text = 'Selecting...';
copyMessage = null;
copyState = '';
} else {
text = 'Mouse down to start the selection, Mouse up to end the selection and copy the selected area.';
animation = true;
}
// Fill tip text
api.ctx.save();
api.ctx.font = '16px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif';
var opacity = animation == true ? getAnimateData(tipTextKeyframes, 'opacity', Date.now() - start, 1500) : 1;
api.ctx.fillStyle = copyState == 'ok' ? 'rgb(0 231 34)' : copyState == 'error' ? 'rgb(255 0 17)' : `rgba(141,141,141,${opacity})`;
api.ctx.textAlign = 'center';
api.ctx.textBaseline = 'bottom';
api.ctx.fillText(text, api.canvas.offsetWidth / 2, api.canvas.offsetHeight - 30);
api.ctx.restore();
if (mouseDown == true) {
// Fill selected area
api.ctx.save();
api.ctx.beginPath();
api.ctx.fillStyle = 'rgba(0, 0, 255, 0.15)';
api.ctx.strokeStyle = '#a570ff';
api.ctx.lineWidth = .75;
api.ctx.fillRect(xDown, yDown, xMove - xDown, yMove - yDown);
api.ctx.strokeRect(xDown, yDown, xMove - xDown, yMove - yDown);
api.ctx.closePath();
api.ctx.restore();
} else if (clip == true) {
// Get the image data and copy to clipboard
clip = false;
var width = xMove - xDown;
var height = yMove - yDown;
if (width == 0 || height == 0) return proccessingImage = false;
var dataUrl = await cutImageFromCanvas(api.canvas, xDown, yDown, xMove - xDown, yMove - yDown);
copyImgToClipboard(dataUrl);
}
},
zIndex: 9999999
}
export default copySelectedArea;
Create a label that follows the mouse and displays its coordinates
mouse-coordinate.js
var position = {
x: 0,
y: 0
}
const mouseCoordinate = {
name: 'mouse-coordinate',
init: (api) => {
// Add listener
api.on('mousemove', (e) => {
// Save the mouse position
position = {
x: e.x,
y: e.y
}
})
},
execute: (api) => {
CanvasRenderingContext2D.prototype.roundRect = function (x, y, width, height, radius, config = {}) {
api.ctx.save();
api.ctx.beginPath();
api.ctx.moveTo(x + radius, y);
api.ctx.lineTo(x + width - radius, y);
api.ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
api.ctx.lineTo(x + width, y + height - radius);
api.ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
api.ctx.lineTo(x + radius, y + height);
api.ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
api.ctx.lineTo(x, y + radius);
api.ctx.quadraticCurveTo(x, y, x + radius, y);
api.ctx.closePath();
api.ctx.fillStyle = config.background || "#000";
api.ctx.strokeStyle = config.border || "#000";
api.ctx.fill();
api.ctx.stroke();
api.ctx.restore();
}
CanvasRenderingContext2D.prototype.textBlock = function (text, x, y, padding, radius, config = {}) {
this.save();
this.beginPath();
this.font = `${config.fontSize}px ${config.fontFamily}`;
this.textBaseline = 'middle';
const textMetrics = this.measureText(text);
const width = textMetrics.width;
const height = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent;
const rectSize = {
width: width + padding.left + padding.right,
height: height + padding.top + padding.bottom
}
if (config.align == 'center') {
this.roundRect(x - rectSize.width / 2, y, rectSize.width, rectSize.height, radius, config);
this.textAlign = 'center';
this.fillText(text, x, y + (height + padding.top + padding.bottom) / 2);
} else if (config.align == 'right') {
this.roundRect(x - rectSize.width, y, rectSize.width, rectSize.height, radius, config);
this.fillText(text, x - rectSize.width + padding.left, y + (height + padding.top + padding.bottom) / 2);
} else {
this.roundRect(x, y, width + padding.left + padding.right, height + padding.top + padding.bottom, radius, config);
this.fillText(text, x + padding.left, y + (height + padding.top + padding.bottom) / 2);
}
this.restore();
}
// Access turtle canvas
if (api.canvas.style.cursor != 'none') {
api.canvas.style.cursor = 'none';
}
const cursorSize = 12;
// Access turtle CanvasRenderingContext2D
api.ctx.save();
var path = new Path2D();
var points = [[0, 0], [3, 1.2], [1.57, 1.57], [1.2, 3], [0, 0]];
var minX = 0,
minY = 0,
maxX = 0,
maxY = 0;
points.forEach(point => {
minX = Math.min(minX, point[0]);
minY = Math.min(minY, point[1]);
maxX = Math.max(maxX, point[0]);
maxY = Math.max(maxY, point[1]);
})
path.moveTo(points[0][0], points[0][1]);
points.forEach(arr => {
path.lineTo(arr[0], arr[1]);
})
api.ctx.translate(position.x, position.y);
const size = {
width: maxX - minX,
height: maxY - minY
}
const scaleFactor = cursorSize / Math.max(size.width, size.height);
api.ctx.transform(scaleFactor, 0, 0, scaleFactor, 0, 0);
api.ctx.fillStyle = '#fbb1ff';
api.ctx.fill(path);
api.ctx.restore();
const xDistance = cursorSize / 2 + 4;
const yDistance = cursorSize / 2 + 4;
api.ctx.textBlock(`(${position.x}, ${position.y})`, position.x + xDistance, position.y + yDistance, {
left: 8,
right: 8,
top: 6,
bottom: 6
}, 6, {
fontSize: 12,
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif',
background: '#fbb1ff',
border: '#fbb1ff',
align: 'left'
});
},
// Options
zIndex: 9999999999
}
// Export plugin
export default mouseCoordinate;