Docs
Examples

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;