Created
April 30, 2024 17:39
-
-
Save cristime/6da1552587aaeee4c3d6e4c3fce1a53f to your computer and use it in GitHub Desktop.
vscode trailcursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // https://www.reddit.com/r/vscode/comments/11e66xh/i_made_neovide_alike_cursor_effect_on_vscode/ | |
| // Configuration | |
| // Set the color of the cursor trail to match the user's cursor color | |
| const Color = "#A052FF" // If set to "default," it will use the theme's cursor color. | |
| // ! default will only reference editorCursor.background | |
| // "workbench.colorCustomizations": { | |
| // "editorCursor.background": "#A052FF", | |
| // } | |
| // Set the style of the cursor to either a line or block | |
| // line option use fill() to draw trail, block option use lineTo to draw trail | |
| const CursorStyle = "block" // Options are 'line' or 'block' | |
| // Set the length of the cursor trail. A higher value may cause lag. | |
| const TrailLength = 8 // Recommended value is around 8 | |
| // Set the polling rate for handling cursor created and destroyed events, in milliseconds. | |
| const CursorUpdatePollingRate = 500 // Recommended value is around 500 | |
| // Use shadow | |
| const UseShadow = false | |
| const ShadowColor = Color | |
| const ShadowBlur = 15 | |
| // imported from https://github.com/tholman/cursor-effects/blob/master/src/rainbowCursor.js | |
| function createTrail(options) { | |
| const totalParticles = options?.length || 20 | |
| let particlesColor = options?.color || "#A052FF" | |
| const style = options?.style || "block" | |
| const canvas = options?.canvas | |
| const context = canvas.getContext("2d") | |
| let cursor = { x: 0, y: 0 } | |
| let particles = [] | |
| let width,height | |
| let sizeX = options?.size || 3 | |
| let sizeY = options?.sizeY || sizeX*2.2 | |
| let cursorsInitted = false | |
| // update canvas size | |
| function updateSize(x,y) { | |
| width = x | |
| height = y | |
| canvas.width = x | |
| canvas.height = y | |
| } | |
| // update cursor position | |
| function move(x,y) { | |
| x = x + sizeX/2 | |
| cursor.x = x | |
| cursor.y = y | |
| if (cursorsInitted === false) { | |
| cursorsInitted = true | |
| for (let i = 0; i < totalParticles; i++) { | |
| addParticle(x, y) | |
| } | |
| } | |
| } | |
| // particle class | |
| class Particle { | |
| constructor(x, y) { | |
| this.position = { x: x, y: y } | |
| } | |
| } | |
| function addParticle(x, y, image) { | |
| particles.push(new Particle(x, y, image)) | |
| } | |
| function calculatePosition() { | |
| let x = cursor.x,y = cursor.y | |
| for (const particleIndex in particles) { | |
| const nextParticlePos = (particles[+particleIndex + 1] || particles[0]).position | |
| const particlePos = particles[+particleIndex].position | |
| particlePos.x = x; | |
| particlePos.y = y; | |
| x += (nextParticlePos.x - particlePos.x) * 0.42 | |
| y += (nextParticlePos.y - particlePos.y) * 0.35 | |
| } | |
| } | |
| // for block cursor | |
| function drawLines() { | |
| context.beginPath() | |
| context.lineJoin = "round" | |
| context.strokeStyle = particlesColor | |
| const lineWidth = Math.min(sizeX,sizeY) | |
| context.lineWidth = lineWidth | |
| if (UseShadow) { | |
| context.shadowColor = ShadowColor; | |
| context.shadowBlur = ShadowBlur; | |
| } | |
| // draw 3 lines | |
| let ymut = (sizeY-lineWidth)/3 | |
| for (let yoffset=0;yoffset<=3;yoffset++) { | |
| let offset = yoffset*ymut | |
| for (const particleIndex in particles) { | |
| const pos = particles[particleIndex].position | |
| if (particleIndex == 0) { | |
| context.moveTo(pos.x, pos.y + offset + lineWidth/2) | |
| } else { | |
| context.lineTo(pos.x, pos.y + offset + lineWidth/2) | |
| } | |
| } | |
| } | |
| context.stroke() | |
| } | |
| // for line cursor | |
| function drawPath() { | |
| context.beginPath() | |
| context.fillStyle = particlesColor | |
| if (UseShadow) { | |
| context.shadowColor = ShadowColor; | |
| context.shadowBlur = ShadowBlur; | |
| } | |
| // draw path | |
| for (let particleIndex=0;particleIndex<totalParticles;particleIndex++) { | |
| const pos = particles[+particleIndex].position | |
| if (particleIndex == 0) { | |
| context.moveTo(pos.x, pos.y) | |
| } else { | |
| context.lineTo(pos.x, pos.y) | |
| } | |
| } | |
| for (let particleIndex=totalParticles-1;particleIndex>=0;particleIndex--) { | |
| const pos = particles[+particleIndex].position | |
| context.lineTo(pos.x, pos.y+sizeY) | |
| } | |
| context.closePath() | |
| context.fill() | |
| context.beginPath() | |
| context.lineJoin = "round" | |
| context.strokeStyle = particlesColor | |
| context.lineWidth = Math.min(sizeX,sizeY) | |
| // for up&down | |
| let offset = -sizeX/2 + sizeY/2 | |
| for (const particleIndex in particles) { | |
| const pos = particles[particleIndex].position | |
| if (particleIndex == 0) { | |
| context.moveTo(pos.x, pos.y + offset) | |
| } else { | |
| context.lineTo(pos.x, pos.y + offset) | |
| } | |
| } | |
| context.stroke() | |
| } | |
| function updateParticles() { | |
| if (!cursorsInitted) return | |
| context.clearRect(0, 0, width, height) | |
| calculatePosition() | |
| if (style=="line") drawPath() | |
| else if (style=="block") drawLines() | |
| } | |
| function updateCursorSize(newSize,newSizeY) { | |
| sizeX = newSize | |
| if (newSizeY) sizeY = newSizeY | |
| } | |
| return { | |
| updateParticles: updateParticles, | |
| move: move, | |
| updateSize: updateSize, | |
| updateCursorSize: updateCursorSize | |
| } | |
| } | |
| // cursor create/remove/move event handler | |
| // by qwreey | |
| // (very dirty but may working) | |
| async function createCursorHandler(handlerFunctions) { | |
| // Get Editor with dirty way (... due to vscode plugin api's limit) | |
| /** @type { Element } */ | |
| let editor | |
| while (!editor) { | |
| await new Promise(resolve=>setTimeout(resolve, 100)) | |
| editor = document.querySelector(".part.editor") | |
| } | |
| handlerFunctions?.onStarted(editor) | |
| // cursor cache | |
| let updateHandlers = [] | |
| let cursorId = 0 | |
| let lastObjects = {} | |
| let lastCursor = 0 | |
| // cursor update handler | |
| function createCursorUpdateHandler(target,cursorId,cursorHolder,minimap) { | |
| let lastX,lastY // save last position | |
| let update = (editorX,editorY)=>{ | |
| // If cursor was destroyed, remove update handler | |
| if (!lastObjects[cursorId]) { | |
| updateHandlers.splice(updateHandlers.indexOf(update),1) | |
| return | |
| } | |
| // get cursor position | |
| let {left:newX,top:newY} = target.getBoundingClientRect() | |
| let revX = newX-editorX,revY = newY-editorY | |
| // if have no changes, ignore | |
| if (revX == lastX && revY == lastY && lastCursor == cursorId) return | |
| lastX = revX;lastY = revY // update last position | |
| // wrong position | |
| if (revX<=0 || revY<=0) return | |
| // if it is invisible, ignore | |
| if (target.style.visibility == "hidden") return | |
| // if moved over minimap, ignore | |
| if (minimap && minimap.offsetWidth != 0 && minimap.getBoundingClientRect().left <= newX) return | |
| // if cursor is not displayed on screen, ignore | |
| if (cursorHolder.getBoundingClientRect().left > newX) return | |
| // update corsor position | |
| lastCursor = cursorId | |
| handlerFunctions?.onCursorPositionUpdated(revX,revY) | |
| handlerFunctions?.onCursorSizeUpdated(target.clientWidth,target.clientHeight) | |
| } | |
| updateHandlers.push(update) | |
| } | |
| // handle cursor create/destroy event (using polling, due to event handlers are LAGGY) | |
| let lastVisibility = "hidden" | |
| setInterval(async ()=>{ | |
| let now = [],count = 0 | |
| // created | |
| for (const target of editor.getElementsByClassName("cursor")) { | |
| if (target.style.visibility != "hidden") count++ | |
| if (target.hasAttribute("cursorId")) { | |
| now.push(+target.getAttribute("cursorId")) | |
| continue | |
| } | |
| let thisCursorId = cursorId++ | |
| now.push(thisCursorId) | |
| lastObjects[thisCursorId] = target | |
| target.setAttribute("cursorId",thisCursorId) | |
| let cursorHolder = target.parentElement.parentElement.parentElement | |
| let minimap = cursorHolder.parentElement.querySelector(".minimap") | |
| createCursorUpdateHandler(target,thisCursorId,cursorHolder,minimap) | |
| // console.log("DEBUG-CursorCreated",thisCursorId) | |
| } | |
| // update visible | |
| let visibility = count<=1 ? "visible" : "hidden" | |
| if (visibility != lastVisibility) { | |
| handlerFunctions?.onCursorVisibilityChanged(visibility) | |
| lastVisibility = visibility | |
| } | |
| // destroyed | |
| for (const id in lastObjects) { | |
| if (now.includes(+id)) continue | |
| delete lastObjects[+id] | |
| // console.log("DEBUG-CursorRemoved",+id) | |
| } | |
| },handlerFunctions?.cursorUpdatePollingRate || 500) | |
| // read cursor position polling | |
| function updateLoop() { | |
| let {left:editorX,top:editorY} = editor.getBoundingClientRect() | |
| for (handler of updateHandlers) handler(editorX,editorY) | |
| handlerFunctions?.onLoop() | |
| requestAnimationFrame(updateLoop) | |
| } | |
| // handle editor view size changed event | |
| function updateEditorSize() { | |
| handlerFunctions?.onEditorSizeUpdated(editor.clientWidth,editor.clientHeight) | |
| } | |
| new ResizeObserver(updateEditorSize).observe(editor) | |
| updateEditorSize() | |
| // startup | |
| updateLoop() | |
| handlerFunctions?.onReady() | |
| } | |
| // Main handler code | |
| let cursorCanvas,rainbowCursorHandle | |
| createCursorHandler({ | |
| // cursor create/destroy event handler polling rate | |
| cursorUpdatePollingRate: CursorUpdatePollingRate, | |
| // When editor instance stared | |
| onStarted: (editor)=>{ | |
| // create new canvas for make animation | |
| cursorCanvas = document.createElement("canvas") | |
| cursorCanvas.style.pointerEvents = "none" | |
| cursorCanvas.style.position = "absolute" | |
| cursorCanvas.style.top = "0px" | |
| cursorCanvas.style.left = "0px" | |
| cursorCanvas.style.zIndex = "1000" | |
| editor.appendChild(cursorCanvas) | |
| // create rainbow cursor effect | |
| // thanks to https://github.com/tholman/cursor-effects/blob/master/src/rainbowCursor.js | |
| // we can create trail effect! | |
| let color = Color | |
| if (color == "default") { | |
| color = getComputedStyle( | |
| document.querySelector("body>.monaco-workbench")) | |
| .getPropertyValue("--vscode-editorCursor-background") | |
| .trim() | |
| } | |
| rainbowCursorHandle = createTrail({ | |
| length: TrailLength, | |
| color: color, | |
| size: 7, | |
| style: CursorStyle, | |
| canvas: cursorCanvas | |
| }) | |
| }, | |
| onReady:()=>{}, | |
| // when cursor moved | |
| onCursorPositionUpdated: (x,y)=>{ | |
| rainbowCursorHandle.move(x,y) | |
| }, | |
| // when editor view size changed | |
| onEditorSizeUpdated: (x,y)=>{ | |
| rainbowCursorHandle.updateSize(x,y) | |
| }, | |
| // when cursor size changed (emoji, ...) | |
| onCursorSizeUpdated: (x,y)=>{ | |
| rainbowCursorHandle.updateCursorSize(x,y) | |
| // rainbowCursorHandle.updateCursorSize(parseInt(y/lineHeight)) | |
| }, | |
| // when using multi cursor... just hide all | |
| onCursorVisibilityChanged: (visibility)=>{ | |
| cursorCanvas.style.visibility = visibility | |
| }, | |
| // update animation | |
| onLoop: ()=>{ | |
| rainbowCursorHandle.updateParticles() | |
| }, | |
| }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment