From 25001d5b39cec25c0f20a8b597dcb81944ed9f53 Mon Sep 17 00:00:00 2001 From: upgradeQ <18361586+upgradeQ@users.noreply.github.com> Date: Fri, 11 Jun 2021 01:09:41 +0300 Subject: [PATCH] Add web rendered trail,remove crop --- README.md | 31 +- cursor_effects_ported/buble.html | 18 + .../cursor-effects-master/src/bubbleCursor.js | 150 ++++++++ .../cursor-effects-master/src/emojiCursor.js | 191 ++++++++++ .../src/fairyDustCursor.js | 188 ++++++++++ .../cursor-effects-master/src/ghostCursor.js | 131 +++++++ .../src/snowflakeCursor.js | 182 ++++++++++ .../src/springyEmojiCursor.js | 241 +++++++++++++ cursor_effects_ported/elastic_emoji.html | 18 + cursor_effects_ported/emoji.html | 18 + cursor_effects_ported/fairy.html | 18 + cursor_effects_ported/ghost.html | 18 + cursor_effects_ported/snowflakes.html | 18 + mouse_skin_obs.py | 327 ++++++++++++------ 14 files changed, 1427 insertions(+), 122 deletions(-) create mode 100644 cursor_effects_ported/buble.html create mode 100644 cursor_effects_ported/cursor-effects-master/src/bubbleCursor.js create mode 100644 cursor_effects_ported/cursor-effects-master/src/emojiCursor.js create mode 100644 cursor_effects_ported/cursor-effects-master/src/fairyDustCursor.js create mode 100644 cursor_effects_ported/cursor-effects-master/src/ghostCursor.js create mode 100644 cursor_effects_ported/cursor-effects-master/src/snowflakeCursor.js create mode 100644 cursor_effects_ported/cursor-effects-master/src/springyEmojiCursor.js create mode 100644 cursor_effects_ported/elastic_emoji.html create mode 100644 cursor_effects_ported/emoji.html create mode 100644 cursor_effects_ported/fairy.html create mode 100644 cursor_effects_ported/ghost.html create mode 100644 cursor_effects_ported/snowflakes.html diff --git a/README.md b/README.md index 3ad03b0..cf2cabf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ -Fork by 3_4_700, original by upgradeQ -The main purpose of this fork is to be able to make a precise custom cursor, like a handdrawn arm holding a pen for artists - # OBS Studio Cursor skin Selected source will follow mouse pointer. Using [`obs_sceneitem_set_pos`](https://obsproject.com/docs/reference-scenes.html#c.obs_sceneitem_set_pos) @@ -11,16 +8,15 @@ Using [`obs_sceneitem_set_pos`](https://obsproject.com/docs/reference-scenes.htm # Limitations - Multiple monitors setup will not work . - If used in fullscreen apps, offset will appear. -- Current code only works for 1920x1080 res # Usage - Create a _source_ with desired cursor(e.g Image source or Media source). - In scripts select _that_ source name. -- Make a group, add Display Capture, Window Capture +- Make a group, add Display Capture, Window Capture. ![img](https://i.imgur.com/CHuLwmp.png) - To crop, crop the _group_, the size should still have the same ratio as your monitor even if you scale it -- To set offset/calibrate, use the Display Capture to see mouse and adjust it at Scripts (or use Tab/Shift+tab to navigate if in Window Capture to not move mouse). You have to do this every time you change the Group scale/move the Group +- To set offset/calibrate, use the Display Capture to see mouse and adjust it at Scripts (or use Tab/Shift+tab to navigate, if in Window Capture, to not move mouse). You have to do this every time you change the Group scale/move it ![img](https://user-images.githubusercontent.com/66927691/121442471-56133280-c9be-11eb-9bb4-ad12b2e4ebfb.jpg) @@ -28,14 +24,11 @@ Using [`obs_sceneitem_set_pos`](https://obsproject.com/docs/reference-scenes.htm - Test it: press Start, press Stop, tweak refresh rate. -# Crop auto update -Zoom or higlight. -- Create 2 display captures. -- Create crop filter with this name: `cropXY`. -- Check relative. -- Set Width and Height to relatively small numbers e.g : 64x64 . -- Image mask blend + color correction might be an option too. -- Run script,select this source as cursor source , check Update crop, click start. + +# Web rendered mouse cursor trails +- Add browser source with mouse tracking local or online web page. +- Make sure to set resolution as your monitor (base) +- Fill all entries, check `Use browser source` # Zoom > Have you ever needed to zoom in on your screen to show some fine detail work, @@ -53,6 +46,16 @@ They all have some level of transparency. ![img](https://i.imgur.com/8qoRU3i.png) - green circle ![Imgur](https://i.imgur.com/s3jvZP5.png) + +# On the Roadmap +- Visual indicator of mouse up/down state. +- Lua based shaders rendering (on mouse up, down, trail, etc...) +- Custom web page rendering (on mouse up, down, trail, etc...) + +# Acknowledgments +- [`3_4_700`](https://github.com/34700) added offsets functionality for precise custom cursor(like a hand drawn arm holding a pen for artists) +- [`tholman/cursor-effects`](https://github.com/tholman/cursor-effects) - stock cursor trails + # Contribute [Forks](https://help.github.com/articles/fork-a-repo) are a great way to contribute to a repository. After forking a repository, you can send the original author a [pull request](https://help.github.com/articles/using-pull-requests) diff --git a/cursor_effects_ported/buble.html b/cursor_effects_ported/buble.html new file mode 100644 index 0000000..b665844 --- /dev/null +++ b/cursor_effects_ported/buble.html @@ -0,0 +1,18 @@ + + + + + Buble cursor + + + + + + + diff --git a/cursor_effects_ported/cursor-effects-master/src/bubbleCursor.js b/cursor_effects_ported/cursor-effects-master/src/bubbleCursor.js new file mode 100644 index 0000000..f02557f --- /dev/null +++ b/cursor_effects_ported/cursor-effects-master/src/bubbleCursor.js @@ -0,0 +1,150 @@ +function bubbleCursor(options) { + let hasWrapperEl = options && options.element + let element = hasWrapperEl || document.body + + let width = window.innerWidth + let height = window.innerHeight + let cursor = { x: width / 2, y: width / 2 } + let particles = [] + let canvas, context + + let canvImages = [] + + function init(wrapperEl) { + canvas = document.createElement("canvas") + context = canvas.getContext("2d") + + canvas.style.top = "0px" + canvas.style.left = "0px" + canvas.style.pointerEvents = "none" + + if (hasWrapperEl) { + canvas.style.position = "absolute" + element.appendChild(canvas) + canvas.width = element.clientWidth + canvas.height = element.clientHeight + } else { + canvas.style.position = "fixed" + document.body.appendChild(canvas) + canvas.width = width + canvas.height = height + } + + bindEvents() + loop() + } + + // Bind events that are needed + function bindEvents() { + element.addEventListener("mousemove", onMouseMove) + element.addEventListener("touchmove", onTouchMove) + element.addEventListener("touchstart", onTouchMove) + window.addEventListener("resize", onWindowResize) + } + + function onWindowResize(e) { + width = window.innerWidth + height = window.innerHeight + + if (hasWrapperEl) { + canvas.width = element.clientWidth + canvas.height = element.clientHeight + } else { + canvas.width = width + canvas.height = height + } + } + + function onTouchMove(e) { + if (e.touches.length > 0) { + for (let i = 0; i < e.touches.length; i++) { + addParticle( + e.touches[i].clientX, + e.touches[i].clientY, + canvImages[Math.floor(Math.random() * canvImages.length)] + ) + } + } + } + + function onMouseMove(e) { + if (hasWrapperEl) { + const boundingRect = element.getBoundingClientRect() + cursor.x = e.clientX - boundingRect.left + cursor.y = e.clientY - boundingRect.top + } else { + cursor.x = e.clientX + cursor.y = e.clientY + } + + addParticle(cursor.x, cursor.y) + } + + function addParticle(x, y, img) { + particles.push(new Particle(x, y, img)) + } + + function updateParticles() { + context.clearRect(0, 0, width, height) + + // Update + for (let i = 0; i < particles.length; i++) { + particles[i].update(context) + } + + // Remove dead particles + for (let i = particles.length - 1; i >= 0; i--) { + if (particles[i].lifeSpan < 0) { + particles.splice(i, 1) + } + } + } + + function loop() { + updateParticles() + requestAnimationFrame(loop) + } + + function Particle(x, y, canvasItem) { + const lifeSpan = Math.floor(Math.random() * 60 + 60) + this.initialLifeSpan = lifeSpan // + this.lifeSpan = lifeSpan //ms + this.velocity = { + x: (Math.random() < 0.5 ? -1 : 1) * (Math.random() / 10), + y: -0.4 + Math.random() * -1, + } + this.position = { x: x, y: y } + this.canv = canvasItem + + this.baseDimension = 4 + + this.update = function(context) { + this.position.x += this.velocity.x + this.position.y += this.velocity.y + this.velocity.x += ((Math.random() < 0.5 ? -1 : 1) * 2) / 75 + this.velocity.y -= Math.random() / 600 + this.lifeSpan-- + + const scale = + 0.2 + (this.initialLifeSpan - this.lifeSpan) / this.initialLifeSpan + + context.fillStyle = "#e6f1f7" + context.strokeStyle = "#3a92c5" + context.beginPath() + context.arc( + this.position.x - (this.baseDimension / 2) * scale, + this.position.y - this.baseDimension / 2, + this.baseDimension * scale, + 0, + 2 * Math.PI + ) + + context.stroke() + context.fill() + + context.closePath() + } + } + + init() +} diff --git a/cursor_effects_ported/cursor-effects-master/src/emojiCursor.js b/cursor_effects_ported/cursor-effects-master/src/emojiCursor.js new file mode 100644 index 0000000..c57069a --- /dev/null +++ b/cursor_effects_ported/cursor-effects-master/src/emojiCursor.js @@ -0,0 +1,191 @@ +function emojiCursor(options) { + const possibleEmoji = (options && options.emoji) || ["😀", "😂", "😆", "😊"] + let hasWrapperEl = options && options.element + let element = hasWrapperEl || document.body + + let width = window.innerWidth + let height = window.innerHeight + const cursor = { x: width / 2, y: width / 2 } + const lastPos = { x: width / 2, y: width / 2 } + let lastTimestamp = 0 + const particles = [] + const canvImages = [] + let canvas, context + + function init() { + canvas = document.createElement("canvas") + context = canvas.getContext("2d") + + canvas.style.top = "0px" + canvas.style.left = "0px" + canvas.style.pointerEvents = "none" + + if (hasWrapperEl) { + canvas.style.position = "absolute" + element.appendChild(canvas) + canvas.width = element.clientWidth + canvas.height = element.clientHeight + } else { + canvas.style.position = "fixed" + document.body.appendChild(canvas) + canvas.width = width + canvas.height = height + } + + context.font = "21px serif" + context.textBaseline = "middle" + context.textAlign = "center" + + possibleEmoji.forEach((emoji) => { + let measurements = context.measureText(emoji) + let bgCanvas = document.createElement("canvas") + let bgContext = bgCanvas.getContext("2d") + + bgCanvas.width = measurements.width + bgCanvas.height = measurements.actualBoundingBoxAscent * 2 + + bgContext.textAlign = "center" + bgContext.font = "21px serif" + bgContext.textBaseline = "middle" + bgContext.fillText( + emoji, + bgCanvas.width / 2, + measurements.actualBoundingBoxAscent + ) + + canvImages.push(bgCanvas) + }) + + bindEvents() + loop() + } + + // Bind events that are needed + function bindEvents() { + element.addEventListener("mousemove", onMouseMove, { passive: true }) + element.addEventListener("touchmove", onTouchMove, { passive: true }) + element.addEventListener("touchstart", onTouchMove, { passive: true }) + window.addEventListener("resize", onWindowResize) + } + + function onWindowResize(e) { + width = window.innerWidth + height = window.innerHeight + + if (hasWrapperEl) { + canvas.width = element.clientWidth + canvas.height = element.clientHeight + } else { + canvas.width = width + canvas.height = height + } + } + + function onTouchMove(e) { + if (e.touches.length > 0) { + for (let i = 0; i < e.touches.length; i++) { + addParticle( + e.touches[i].clientX, + e.touches[i].clientY, + canvImages[Math.floor(Math.random() * canvImages.length)] + ) + } + } + } + + function onMouseMove(e) { + // Dont run too fast + if (e.timeStamp - lastTimestamp < 16) { + return + } + + window.requestAnimationFrame(() => { + if (hasWrapperEl) { + const boundingRect = element.getBoundingClientRect() + cursor.x = e.clientX - boundingRect.left + cursor.y = e.clientY - boundingRect.top + } else { + cursor.x = e.clientX + cursor.y = e.clientY + } + + const distBetweenPoints = Math.hypot( + cursor.x - lastPos.x, + cursor.y - lastPos.y + ) + + if (distBetweenPoints > 1) { + addParticle( + cursor.x, + cursor.y, + canvImages[Math.floor(Math.random() * possibleEmoji.length)] + ) + + lastPos.x = cursor.x + lastPos.y = cursor.y + lastTimestamp = e.timeStamp + } + }) + } + + function addParticle(x, y, img) { + particles.push(new Particle(x, y, img)) + } + + function updateParticles() { + context.clearRect(0, 0, width, height) + + // Update + for (let i = 0; i < particles.length; i++) { + particles[i].update(context) + } + + // Remove dead particles + for (let i = particles.length - 1; i >= 0; i--) { + if (particles[i].lifeSpan < 0) { + particles.splice(i, 1) + } + } + } + + function loop() { + updateParticles() + requestAnimationFrame(loop) + } + + /** + * Particles + */ + + function Particle(x, y, canvasItem) { + const lifeSpan = Math.floor(Math.random() * 60 + 80) + this.initialLifeSpan = lifeSpan // + this.lifeSpan = lifeSpan //ms + this.velocity = { + x: (Math.random() < 0.5 ? -1 : 1) * (Math.random() / 2), + y: Math.random() * 0.4 + 0.8, + } + this.position = { x: x, y: y } + this.canv = canvasItem + + this.update = function(context) { + this.position.x += this.velocity.x + this.position.y += this.velocity.y + this.lifeSpan-- + + this.velocity.y += 0.05 + + const scale = Math.max(this.lifeSpan / this.initialLifeSpan, 0) + + context.drawImage( + this.canv, + this.position.x - (this.canv.width / 2) * scale, + this.position.y - this.canv.height / 2, + this.canv.width * scale, + this.canv.height * scale + ) + } + } + + init() +} diff --git a/cursor_effects_ported/cursor-effects-master/src/fairyDustCursor.js b/cursor_effects_ported/cursor-effects-master/src/fairyDustCursor.js new file mode 100644 index 0000000..62f2c91 --- /dev/null +++ b/cursor_effects_ported/cursor-effects-master/src/fairyDustCursor.js @@ -0,0 +1,188 @@ +function fairyDustCursor(options) { + let possibleColors = (options && options.colors) || [ + "#D61C59", + "#E7D84B", + "#1B8798", + ] + let hasWrapperEl = options && options.element + let element = hasWrapperEl || document.body + + let width = window.innerWidth + let height = window.innerHeight + const cursor = { x: width / 2, y: width / 2 } + const lastPos = { x: width / 2, y: width / 2 } + const particles = [] + const canvImages = [] + let canvas, context + + const char = "*" + + function init() { + canvas = document.createElement("canvas") + context = canvas.getContext("2d") + canvas.style.top = "0px" + canvas.style.left = "0px" + canvas.style.pointerEvents = "none" + + if (hasWrapperEl) { + canvas.style.position = "absolute" + element.appendChild(canvas) + canvas.width = element.clientWidth + canvas.height = element.clientHeight + } else { + canvas.style.position = "fixed" + element.appendChild(canvas) + canvas.width = width + canvas.height = height + } + + context.font = "21px serif" + context.textBaseline = "middle" + context.textAlign = "center" + + possibleColors.forEach((color) => { + let measurements = context.measureText(char) + let bgCanvas = document.createElement("canvas") + let bgContext = bgCanvas.getContext("2d") + + bgCanvas.width = measurements.width + bgCanvas.height = + measurements.actualBoundingBoxAscent + + measurements.actualBoundingBoxDescent + + bgContext.fillStyle = color + bgContext.textAlign = "center" + bgContext.font = "21px serif" + bgContext.textBaseline = "middle" + bgContext.fillText( + char, + bgCanvas.width / 2, + measurements.actualBoundingBoxAscent + ) + + canvImages.push(bgCanvas) + }) + + bindEvents() + loop() + } + + // Bind events that are needed + function bindEvents() { + element.addEventListener("mousemove", onMouseMove) + element.addEventListener("touchmove", onTouchMove) + element.addEventListener("touchstart", onTouchMove) + window.addEventListener("resize", onWindowResize) + } + + function onWindowResize(e) { + width = window.innerWidth + height = window.innerHeight + + if (hasWrapperEl) { + canvas.width = element.clientWidth + canvas.height = element.clientHeight + } else { + canvas.width = width + canvas.height = height + } + } + + function onTouchMove(e) { + if (e.touches.length > 0) { + for (let i = 0; i < e.touches.length; i++) { + addParticle( + e.touches[i].clientX, + e.touches[i].clientY, + canvImages[Math.floor(Math.random() * canvImages.length)] + ) + } + } + } + + function onMouseMove(e) { + window.requestAnimationFrame(() => { + if (hasWrapperEl) { + const boundingRect = element.getBoundingClientRect() + cursor.x = e.clientX - boundingRect.left + cursor.y = e.clientY - boundingRect.top + } else { + cursor.x = e.clientX + cursor.y = e.clientY + } + + const distBetweenPoints = Math.hypot( + cursor.x - lastPos.x, + cursor.y - lastPos.y + ) + + if (distBetweenPoints > 1.5) { + addParticle( + cursor.x, + cursor.y, + canvImages[Math.floor(Math.random() * possibleColors.length)] + ) + + lastPos.x = cursor.x + lastPos.y = cursor.y + } + }) + } + + function addParticle(x, y, color) { + particles.push(new Particle(x, y, color)) + } + + function updateParticles() { + context.clearRect(0, 0, width, height) + + // Update + for (let i = 0; i < particles.length; i++) { + particles[i].update(context) + } + + // Remove dead particles + for (let i = particles.length - 1; i >= 0; i--) { + if (particles[i].lifeSpan < 0) { + particles.splice(i, 1) + } + } + } + + function loop() { + updateParticles() + requestAnimationFrame(loop) + } + + function Particle(x, y, canvasItem) { + const lifeSpan = Math.floor(Math.random() * 30 + 60) + this.initialLifeSpan = lifeSpan // + this.lifeSpan = lifeSpan //ms + this.velocity = { + x: (Math.random() < 0.5 ? -1 : 1) * (Math.random() / 2), + y: Math.random() * 0.7 + 0.9, + } + this.position = { x: x, y: y } + this.canv = canvasItem + + this.update = function(context) { + this.position.x += this.velocity.x + this.position.y += this.velocity.y + this.lifeSpan-- + + this.velocity.y += 0.02 + + const scale = Math.max(this.lifeSpan / this.initialLifeSpan, 0) + + context.drawImage( + this.canv, + this.position.x - (this.canv.width / 2) * scale, + this.position.y - this.canv.height / 2, + this.canv.width * scale, + this.canv.height * scale + ) + } + } + + init() +} diff --git a/cursor_effects_ported/cursor-effects-master/src/ghostCursor.js b/cursor_effects_ported/cursor-effects-master/src/ghostCursor.js new file mode 100644 index 0000000..79cd80d --- /dev/null +++ b/cursor_effects_ported/cursor-effects-master/src/ghostCursor.js @@ -0,0 +1,131 @@ +function ghostCursor(options) { + let hasWrapperEl = options && options.element + let element = hasWrapperEl || document.body + + let width = window.innerWidth + let height = window.innerHeight + let cursor = { x: width / 2, y: width / 2 } + let particles = [] + let canvas, context + + let baseImage = new Image() + baseImage.src = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAATCAYAAACk9eypAAAAAXNSR0IArs4c6QAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAADKADAAQAAAABAAAAEwAAAAAChpcNAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpMwidZAAABqElEQVQoFY3SPUvDQBgH8BREpRHExYiDgmLFl6WC+AYmWeyLg4i7buJX8DMpOujgyxGvUYeCgzhUQUSKKLUS0+ZyptXh8Z5Ti621ekPyJHl+uftfomhaf9Ei5JyxXKfynyEA6EYcLHpwyflT958GAQ7DTABNHd8EbtDbEH2BD5QEQmi2mM8P/Iq+A0SzszEg+3sPjDnDdVEtQKQbMUidHD3xVzf6A9UDEmEm+8h9KTqTVUjT+vB53aHrCbAPiceYq1dQI1Aqv4EhMll0jzv+Y0yiRgCnLRSYyDQHVoqUXe4uKL9l+L7GXC4vkMhE6eW/AOJs9k583ORDUyXMZ8F5SVHVVnllmPNKSFagAJ5DofaqGXw/gHBYg51dIldkmknY3tguv3jOtHR4+MqAzaraJXbEhqHhcQlwGSOi5pytVQHZLN5s0WNe8HPrLYlFsO20RPHkImxsbmHdLJFI76th7Z4SeuF53hTeFLvhRCJRCTKZKxgdnRDbW+iozFJbBMw14/ElwGYc0egMBMFzT21f5Rog33Z7dX02GBm7WV5ZfT5Nn5bE3zuCDe9UxdTpNvK+5AAAAABJRU5ErkJggg==" + + function init() { + canvas = document.createElement("canvas") + context = canvas.getContext("2d") + canvas.style.top = "0px" + canvas.style.left = "0px" + canvas.style.pointerEvents = "none" + + if (hasWrapperEl) { + canvas.style.position = "absolute" + element.appendChild(canvas) + canvas.width = element.clientWidth + canvas.height = element.clientHeight + } else { + canvas.style.position = "fixed" + document.body.appendChild(canvas) + canvas.width = width + canvas.height = height + } + + bindEvents() + loop() + } + + // Bind events that are needed + function bindEvents() { + element.addEventListener("mousemove", onMouseMove) + element.addEventListener("touchmove", onTouchMove) + element.addEventListener("touchstart", onTouchMove) + window.addEventListener("resize", onWindowResize) + } + + function onWindowResize(e) { + width = window.innerWidth + height = window.innerHeight + + if (hasWrapperEl) { + canvas.width = element.clientWidth + canvas.height = element.clientHeight + } else { + canvas.width = width + canvas.height = height + } + } + + function onTouchMove(e) { + if (e.touches.length > 0) { + for (let i = 0; i < e.touches.length; i++) { + addParticle(e.touches[i].clientX, e.touches[i].clientY, baseImage) + } + } + } + + function onMouseMove(e) { + if (hasWrapperEl) { + const boundingRect = element.getBoundingClientRect() + cursor.x = e.clientX - boundingRect.left + cursor.y = e.clientY - boundingRect.top + } else { + cursor.x = e.clientX + cursor.y = e.clientY + } + + addParticle(cursor.x, cursor.y, baseImage) + } + + function addParticle(x, y, image) { + particles.push(new Particle(x, y, image)) + } + + function updateParticles() { + context.clearRect(0, 0, width, height) + + // Update + for (let i = 0; i < particles.length; i++) { + particles[i].update(context) + } + + // Remove dead particles + for (let i = particles.length - 1; i >= 0; i--) { + if (particles[i].lifeSpan < 0) { + particles.splice(i, 1) + } + } + } + + function loop() { + updateParticles() + requestAnimationFrame(loop) + } + + /** + * Particles + */ + + function Particle(x, y, image) { + const lifeSpan = 40 + this.initialLifeSpan = lifeSpan //ms + this.lifeSpan = lifeSpan //ms + this.position = { x: x, y: y } + + this.image = image + + this.update = function(context) { + this.lifeSpan-- + const opacity = Math.max(this.lifeSpan / this.initialLifeSpan, 0) + + context.globalAlpha = opacity + context.drawImage( + this.image, + this.position.x, // - (this.canv.width / 2) * scale, + this.position.y //- this.canv.height / 2, + ) + } + } + + init() +} diff --git a/cursor_effects_ported/cursor-effects-master/src/snowflakeCursor.js b/cursor_effects_ported/cursor-effects-master/src/snowflakeCursor.js new file mode 100644 index 0000000..74cc55b --- /dev/null +++ b/cursor_effects_ported/cursor-effects-master/src/snowflakeCursor.js @@ -0,0 +1,182 @@ +function snowflakeCursor(options) { + let hasWrapperEl = options && options.element + let element = hasWrapperEl || document.body + + let possibleEmoji = ["❄ïļ"] + let width = window.innerWidth + let height = window.innerHeight + let cursor = { x: width / 2, y: width / 2 } + let particles = [] + let canvas, context + + let canvImages = [] + + function init() { + canvas = document.createElement("canvas") + context = canvas.getContext("2d") + + canvas.style.top = "0px" + canvas.style.left = "0px" + canvas.style.pointerEvents = "none" + + if (hasWrapperEl) { + canvas.style.position = "absolute" + element.appendChild(canvas) + canvas.width = element.clientWidth + canvas.height = element.clientHeight + } else { + canvas.style.position = "fixed" + document.body.appendChild(canvas) + canvas.width = width + canvas.height = height + } + + context.font = "12px serif" + context.textBaseline = "middle" + context.textAlign = "center" + + possibleEmoji.forEach((emoji) => { + let measurements = context.measureText(emoji) + let bgCanvas = document.createElement("canvas") + let bgContext = bgCanvas.getContext("2d") + + bgCanvas.width = measurements.width + bgCanvas.height = measurements.actualBoundingBoxAscent * 2 + + bgContext.textAlign = "center" + bgContext.font = "12px serif" + bgContext.textBaseline = "middle" + bgContext.fillText( + emoji, + bgCanvas.width / 2, + measurements.actualBoundingBoxAscent + ) + + canvImages.push(bgCanvas) + }) + + bindEvents() + loop() + } + + // Bind events that are needed + function bindEvents() { + element.addEventListener("mousemove", onMouseMove) + element.addEventListener("touchmove", onTouchMove) + element.addEventListener("touchstart", onTouchMove) + window.addEventListener("resize", onWindowResize) + } + + function onWindowResize(e) { + width = window.innerWidth + height = window.innerHeight + + if (hasWrapperEl) { + canvas.width = element.clientWidth + canvas.height = element.clientHeight + } else { + canvas.width = width + canvas.height = height + } + } + + function onTouchMove(e) { + if (e.touches.length > 0) { + for (let i = 0; i < e.touches.length; i++) { + addParticle( + e.touches[i].clientX, + e.touches[i].clientY, + canvImages[Math.floor(Math.random() * canvImages.length)] + ) + } + } + } + + function onMouseMove(e) { + if (hasWrapperEl) { + const boundingRect = element.getBoundingClientRect() + cursor.x = e.clientX - boundingRect.left + cursor.y = e.clientY - boundingRect.top + } else { + cursor.x = e.clientX + cursor.y = e.clientY + } + + addParticle( + cursor.x, + cursor.y, + canvImages[Math.floor(Math.random() * possibleEmoji.length)] + ) + } + + function addParticle(x, y, img) { + particles.push(new Particle(x, y, img)) + } + + function updateParticles() { + context.clearRect(0, 0, width, height) + + // Update + for (let i = 0; i < particles.length; i++) { + particles[i].update(context) + } + + // Remove dead particles + for (let i = particles.length - 1; i >= 0; i--) { + if (particles[i].lifeSpan < 0) { + particles.splice(i, 1) + } + } + } + + function loop() { + updateParticles() + requestAnimationFrame(loop) + } + + /** + * Particles + */ + + function Particle(x, y, canvasItem) { + const lifeSpan = Math.floor(Math.random() * 60 + 80) + this.initialLifeSpan = lifeSpan // + this.lifeSpan = lifeSpan //ms + this.velocity = { + x: (Math.random() < 0.5 ? -1 : 1) * (Math.random() / 2), + y: 1 + Math.random(), + } + this.position = { x: x, y: y } + this.canv = canvasItem + + this.update = function(context) { + this.position.x += this.velocity.x + this.position.y += this.velocity.y + this.lifeSpan-- + + this.velocity.x += ((Math.random() < 0.5 ? -1 : 1) * 2) / 75 + this.velocity.y -= Math.random() / 300 + + const scale = Math.max(this.lifeSpan / this.initialLifeSpan, 0) + + const degrees = 2 * this.lifeSpan + const radians = degrees * 0.0174533 // not perfect but close enough + + context.translate(this.position.x, this.position.y) + context.rotate(radians) + + context.drawImage( + this.canv, + (-this.canv.width / 2) * scale, + -this.canv.height / 2, + this.canv.width * scale, + this.canv.height * scale + ) + + context.rotate(-radians) + context.translate(-this.position.x, -this.position.y) + } + } + + init() +} diff --git a/cursor_effects_ported/cursor-effects-master/src/springyEmojiCursor.js b/cursor_effects_ported/cursor-effects-master/src/springyEmojiCursor.js new file mode 100644 index 0000000..a260780 --- /dev/null +++ b/cursor_effects_ported/cursor-effects-master/src/springyEmojiCursor.js @@ -0,0 +1,241 @@ +// The springy emoji effect has been translated over from this old +// code, to modern js & canvas +// - http://www.yaldex.com/FSMessages/ElasticBullets.htm +function springyEmojiCursor(options) { + let emoji = (options && options.emoji) || "ðŸĪŠ" + let hasWrapperEl = options && options.element + let element = hasWrapperEl || document.body + + let nDots = 7 + let DELTAT = 0.01 + let SEGLEN = 10 + let SPRINGK = 10 + let MASS = 1 + let GRAVITY = 50 + let RESISTANCE = 10 + let STOPVEL = 0.1 + let STOPACC = 0.1 + let DOTSIZE = 11 + let BOUNCE = 0.7 + + let width = window.innerWidth + let height = window.innerHeight + let cursor = { x: width / 2, y: width / 2 } + let particles = [] + let canvas, context + + let emojiAsImage + + function init() { + canvas = document.createElement("canvas") + context = canvas.getContext("2d") + canvas.style.top = "0px" + canvas.style.left = "0px" + canvas.style.pointerEvents = "none" + + if (hasWrapperEl) { + canvas.style.position = "absolute" + element.appendChild(canvas) + canvas.width = element.clientWidth + canvas.height = element.clientHeight + } else { + canvas.style.position = "fixed" + document.body.appendChild(canvas) + canvas.width = width + canvas.height = height + } + + // Save emoji as an image for performance + context.font = "16px serif" + context.textBaseline = "middle" + context.textAlign = "center" + + let measurements = context.measureText(emoji) + let bgCanvas = document.createElement("canvas") + let bgContext = bgCanvas.getContext("2d") + + bgCanvas.width = measurements.width + bgCanvas.height = measurements.actualBoundingBoxAscent * 2 + + bgContext.textAlign = "center" + bgContext.font = "16px serif" + bgContext.textBaseline = "middle" + bgContext.fillText( + emoji, + bgCanvas.width / 2, + measurements.actualBoundingBoxAscent + ) + + emojiAsImage = bgCanvas + + let i = 0 + for (i = 0; i < nDots; i++) { + particles[i] = new Particle(emojiAsImage) + } + + bindEvents() + loop() + } + + // Bind events that are needed + function bindEvents() { + element.addEventListener("mousemove", onMouseMove) + element.addEventListener("touchmove", onTouchMove) + element.addEventListener("touchstart", onTouchMove) + window.addEventListener("resize", onWindowResize) + } + + function onWindowResize(e) { + width = window.innerWidth + height = window.innerHeight + + if (hasWrapperEl) { + canvas.width = element.clientWidth + canvas.height = element.clientHeight + } else { + canvas.width = width + canvas.height = height + } + } + + function onTouchMove(e) { + if (e.touches.length > 0) { + if (hasWrapperEl) { + const boundingRect = element.getBoundingClientRect() + cursor.x = e.touches[0].clientX - boundingRect.left + cursor.y = e.touches[0].clientY - boundingRect.top + } else { + cursor.x = e.touches[0].clientX + cursor.y = e.touches[0].clientY + } + } + } + + function onMouseMove(e) { + if (hasWrapperEl) { + const boundingRect = element.getBoundingClientRect() + cursor.x = e.clientX - boundingRect.left + cursor.y = e.clientY - boundingRect.top + } else { + cursor.x = e.clientX + cursor.y = e.clientY + } + } + + function updateParticles() { + canvas.width = canvas.width + + // follow mouse + particles[0].position.x = cursor.x + particles[0].position.y = cursor.y + + // Start from 2nd dot + for (i = 1; i < nDots; i++) { + let spring = new vec(0, 0) + + if (i > 0) { + springForce(i - 1, i, spring) + } + + if (i < nDots - 1) { + springForce(i + 1, i, spring) + } + + let resist = new vec( + -particles[i].velocity.x * RESISTANCE, + -particles[i].velocity.y * RESISTANCE + ) + + let accel = new vec( + (spring.X + resist.X) / MASS, + (spring.Y + resist.Y) / MASS + GRAVITY + ) + + particles[i].velocity.x += DELTAT * accel.X + particles[i].velocity.y += DELTAT * accel.Y + + if ( + Math.abs(particles[i].velocity.x) < STOPVEL && + Math.abs(particles[i].velocity.y) < STOPVEL && + Math.abs(accel.X) < STOPACC && + Math.abs(accel.Y) < STOPACC + ) { + particles[i].velocity.x = 0 + particles[i].velocity.y = 0 + } + + particles[i].position.x += particles[i].velocity.x + particles[i].position.y += particles[i].velocity.y + + let height, width + height = canvas.clientHeight + width = canvas.clientWidth + + if (particles[i].position.y >= height - DOTSIZE - 1) { + if (particles[i].velocity.y > 0) { + particles[i].velocity.y = BOUNCE * -particles[i].velocity.y + } + particles[i].position.y = height - DOTSIZE - 1 + } + + if (particles[i].position.x >= width - DOTSIZE) { + if (particles[i].velocity.x > 0) { + particles[i].velocity.x = BOUNCE * -particles[i].velocity.x + } + particles[i].position.x = width - DOTSIZE - 1 + } + + if (particles[i].position.x < 0) { + if (particles[i].velocity.x < 0) { + particles[i].velocity.x = BOUNCE * -particles[i].velocity.x + } + particles[i].position.x = 0 + } + + particles[i].draw(context) + } + } + + function loop() { + updateParticles() + requestAnimationFrame(loop) + } + + function vec(X, Y) { + this.X = X + this.Y = Y + } + + function springForce(i, j, spring) { + let dx = particles[i].position.x - particles[j].position.x + let dy = particles[i].position.y - particles[j].position.y + let len = Math.sqrt(dx * dx + dy * dy) + if (len > SEGLEN) { + let springF = SPRINGK * (len - SEGLEN) + spring.X += (dx / len) * springF + spring.Y += (dy / len) * springF + } + } + + function Particle(canvasItem) { + this.position = { x: cursor.x, y: cursor.y } + this.velocity = { + x: 0, + y: 0, + } + + this.canv = canvasItem + + this.draw = function(context) { + context.drawImage( + this.canv, + this.position.x - this.canv.width / 2, + this.position.y - this.canv.height / 2, + this.canv.width, + this.canv.height + ) + } + } + + init() +} diff --git a/cursor_effects_ported/elastic_emoji.html b/cursor_effects_ported/elastic_emoji.html new file mode 100644 index 0000000..ce8dc12 --- /dev/null +++ b/cursor_effects_ported/elastic_emoji.html @@ -0,0 +1,18 @@ + + + + + Elastic emoji cursor + + + + + + + diff --git a/cursor_effects_ported/emoji.html b/cursor_effects_ported/emoji.html new file mode 100644 index 0000000..f5a1909 --- /dev/null +++ b/cursor_effects_ported/emoji.html @@ -0,0 +1,18 @@ + + + + + Emoji cursor + + + + + + + diff --git a/cursor_effects_ported/fairy.html b/cursor_effects_ported/fairy.html new file mode 100644 index 0000000..bac1f77 --- /dev/null +++ b/cursor_effects_ported/fairy.html @@ -0,0 +1,18 @@ + + + + + Fairy cursor + + + + + + + diff --git a/cursor_effects_ported/ghost.html b/cursor_effects_ported/ghost.html new file mode 100644 index 0000000..720d07a --- /dev/null +++ b/cursor_effects_ported/ghost.html @@ -0,0 +1,18 @@ + + + + + Ghost cursor + + + + + + + diff --git a/cursor_effects_ported/snowflakes.html b/cursor_effects_ported/snowflakes.html new file mode 100644 index 0000000..ea453ee --- /dev/null +++ b/cursor_effects_ported/snowflakes.html @@ -0,0 +1,18 @@ + + + + + Snowflake cursor + + + + + + + diff --git a/mouse_skin_obs.py b/mouse_skin_obs.py index 84aad33..ca6f284 100644 --- a/mouse_skin_obs.py +++ b/mouse_skin_obs.py @@ -1,11 +1,124 @@ -import obspython as obs +import obspython as S # studio +from contextlib import contextmanager, ExitStack +from types import SimpleNamespace as dot from pynput.mouse import Controller # python -m pip install pynput +__version__ = "2.0.0" c = Controller() get_position = lambda: c.position -__version__ = "1.0.0" +@contextmanager +def source_auto_release(source_name): + source = S.obs_get_source_by_name(source_name) + try: + yield source + finally: + S.obs_source_release(source) + + +@contextmanager +def data_ar(source_settings=None): + if source_settings is not None: + settings = S.obs_data_create() + else: + settings = S.obs_source_get_settings(source_settings) + try: + yield settings + finally: + S.obs_data_release(settings) + + +@contextmanager +def scene_from_source_ar(source): + source = S.obs_scene_from_source(source) + try: + yield source + finally: + S.obs_scene_release(source) + + +def get_modifiers(key_modifiers): + if key_modifiers: + shift = key_modifiers.get("shift") + control = key_modifiers.get("control") + alt = key_modifiers.get("alt") + command = key_modifiers.get("command") + else: + shift = control = alt = command = 0 + modifiers = 0 + + if shift: + modifiers |= S.INTERACT_SHIFT_KEY + if control: + modifiers |= S.INTERACT_CONTROL_KEY + if alt: + modifiers |= S.INTERACT_ALT_KEY + if command: + modifiers |= S.INTERACT_COMMAND_KEY + return modifiers + + +def send_mouse_click_to_browser( + source, + x=0, + y=0, + button_type=S.MOUSE_LEFT, + mouse_up=False, + click_count=1, + key_modifiers=None, +): + event = S.obs_mouse_event() + event.modifiers = get_modifiers(key_modifiers) + event.x = x + event.y = y + S.obs_source_send_mouse_click(source, event, button_type, mouse_up, click_count) + + +def send_mouse_move_to_browser( + source, + x=0, + y=0, + key_modifiers=None, +): + event = S.obs_mouse_event() + event.modifiers = get_modifiers(key_modifiers) + event.x = x + event.y = y + S.obs_source_send_mouse_move(source, event, False) # do not leave + + +G = dot() + +G.LMB = G.RMB = G.MOUSE_HOOKED = False + + +def HTK_1_CB(pressed): + G.LMB = pressed + + +def HTK_2_CB(pressed): + G.RMB = pressed + + +def hook_mouse_buttons(): + if G.MOUSE_HOOKED: + raise RuntimeError("already hooked mouse") + key_1 = '{"htk_1_mouse": [ { "key": "OBS_KEY_MOUSE1" } ], ' + key_2 = '"htk_2_mouse": [ { "key": "OBS_KEY_MOUSE2" } ]}' + json_s = key_1 + key_2 + default_hotkeys = [ + dot(id="htk_1_mouse", des="LMB state", callback=HTK_1_CB), + dot(id="htk_2_mouse", des="RMB state", callback=HTK_2_CB), + ] + settings = S.obs_data_create_from_json(json_s) + for k in default_hotkeys: + a = S.obs_data_get_array(settings, k.id) + h = S.obs_hotkey_register_frontend(k.id, k.des, k.callback) + S.obs_hotkey_load(h, a) + S.obs_data_array_release(a) + S.obs_data_release(settings) + G.MOUSE_HOOKED = True def apply_scale(x, y, width, height): @@ -13,97 +126,78 @@ def apply_scale(x, y, width, height): height = round(height * y) return width, height + def lerp(minVal, maxVal, k): - val = minVal + ((maxVal - minVal)*k) + val = minVal + ((maxVal - minVal) * k) return val + class CursorAsSource: source_name = None + target_name = None + browser_source_name = None lock = True flag = True refresh_rate = 15 + width = 1920 + height = 1080 + is_update_browser = False - def update_cursor(self): - source = obs.obs_get_source_by_name(self.source_name) - settings = obs.obs_data_create() + def update_cursor_on_scene(self): + ctx = ExitStack().enter_context + source = ctx(source_auto_release(self.source_name)) + settings = ctx(data_ar()) if source is not None: - scene_source = obs.obs_frontend_get_current_scene() - scene_width = obs.obs_source_get_width(source) - scene_height = obs.obs_source_get_height(source) - scene = obs.obs_scene_from_source(scene_source) - scene_item = obs.obs_scene_find_source(scene, self.source_name) - target_item = obs.obs_scene_find_source(scene, self.target_name) + scene_width = S.obs_source_get_width(source) + scene_height = S.obs_source_get_height(source) + + scene_source = S.obs_frontend_get_current_scene() + scene = ctx(scene_from_source_ar(scene_source)) + scene_item = S.obs_scene_find_source_recursive(scene, self.source_name) + target_item = S.obs_scene_find_source_recursive(scene, self.target_name) if scene_item: - scale = obs.vec2() - obs.obs_sceneitem_get_scale(scene_item, scale) + scale = S.vec2() + S.obs_sceneitem_get_scale(scene_item, scale) scene_width, scene_height = apply_scale( scale.x, scale.y, scene_width, scene_height ) - - next_pos = obs.vec2() + + next_pos = S.vec2() next_pos.x, next_pos.y = get_position() next_pos.x -= self.offset_x next_pos.y -= self.offset_y - ## base: 1920*1080, i should add something to make this automatically change based on the Desktop Capture used - ## maybe make it able to use multiple monitors as well? - ratio_x = next_pos.x/1920 - ratio_y = next_pos.y/1080 - - target_scale = obs.vec2() - target = obs.obs_get_source_by_name(self.target_name) - obs.obs_sceneitem_get_scale(target_item, target_scale) - target_x = obs.obs_source_get_width(target) * target_scale.x - target_y = obs.obs_source_get_height(target) * target_scale.y - + ## TODO maybe make it able to use multiple monitors as well? + ratio_x = next_pos.x / self.width + ratio_y = next_pos.y / self.height + + target_scale = S.vec2() + target = ctx(source_auto_release(self.target_name)) + S.obs_sceneitem_get_scale(target_item, target_scale) + target_x = S.obs_source_get_width(target) * target_scale.x + target_y = S.obs_source_get_height(target) * target_scale.y + next_pos.x = lerp(0, target_x, ratio_x) next_pos.y = lerp(0, target_y, ratio_y) - obs.obs_sceneitem_set_pos(scene_item, next_pos) - - obs.obs_data_release(settings) - obs.obs_scene_release(scene) - obs.obs_source_release(source) - - def update_crop(self): - """ - Create 2 display captures. - Create crop filter with this name: cropXY. - Check relative. - Set Width and Height to relatively small numbers e.g : 64x64 . - Image mask blend + color correction might be an option too. - Run script,select this source as cursor source , check Update crop, click start. - """ - source = obs.obs_get_source_by_name(self.source_name) - crop = obs.obs_source_get_filter_by_name(source, "cropXY") - filter_settings = obs.obs_source_get_settings(crop) - - _x, _y = get_position() - # https://github.com/obsproject/obs-studio/blob/79981889c6d87d6e371e9dc8fcaad36f06eb9c9e/plugins/obs-filters/crop-filter.c#L87-L93 - w = obs.obs_data_get_int(filter_settings, "cx") - h = obs.obs_data_get_int(filter_settings, "cy") - h, w = int(h / 2), int(w / 2) - obs.obs_data_set_int(filter_settings, "left", _x - h) - obs.obs_data_set_int(filter_settings, "top", _y - w) - - obs.obs_source_update(crop, filter_settings) - - obs.obs_data_release(filter_settings) - obs.obs_source_release(source) - obs.obs_source_release(crop) - - def ticker(self): - """ how fast update.One callback at time with lock""" - if self.lock: - if self.update_xy: - self.update_crop() - self.update_cursor() - else: - self.update_cursor() + S.obs_sceneitem_set_pos(scene_item, next_pos) - if not self.lock: - obs.remove_current_callback() + def update_cursor_inside_browser_source(self): + with source_auto_release(self.browser_source_name) as source: + send_mouse_move_to_browser(source, *get_position()) + + def ticker(self): # it is not a thread because obs might not close properly + """how fast update.One callback at time with lock""" + if self.lock: + if self.is_update_browser: + self.update_cursor_inside_browser_source() + return + self.update_cursor_on_scene() + else: + S.remove_current_callback() py_cursor = CursorAsSource() +hook_mouse_buttons() +############### ############### ############### def stop_pressed(props, prop): @@ -112,67 +206,84 @@ def stop_pressed(props, prop): def start_pressed(props, prop): - if py_cursor.source_name != "" and py_cursor.flag: - obs.timer_add(py_cursor.ticker, py_cursor.refresh_rate) + if py_cursor.flag: + S.timer_add(py_cursor.ticker, py_cursor.refresh_rate) py_cursor.lock = True py_cursor.flag = False # to keep only one timer callback def script_defaults(settings): - obs.obs_data_set_default_int(settings, "_refresh_rate", py_cursor.refresh_rate) + S.obs_data_set_default_int(settings, "_refresh_rate", py_cursor.refresh_rate) + S.obs_data_set_default_int(settings, "_width", py_cursor.width) + S.obs_data_set_default_int(settings, "_height", py_cursor.height) def script_update(settings): - py_cursor.update_xy = obs.obs_data_get_bool(settings, "bool_yn") - py_cursor.source_name = obs.obs_data_get_string(settings, "source") - py_cursor.target_name = obs.obs_data_get_string(settings, "target") - py_cursor.refresh_rate = obs.obs_data_get_int(settings, "_refresh_rate") - py_cursor.offset_x = obs.obs_data_get_int(settings, "_offset_x") - py_cursor.offset_y = obs.obs_data_get_int(settings, "_offset_y") + py_cursor.source_name = S.obs_data_get_string(settings, "source") + py_cursor.target_name = S.obs_data_get_string(settings, "target") + py_cursor.refresh_rate = S.obs_data_get_int(settings, "_refresh_rate") + py_cursor.offset_x = S.obs_data_get_int(settings, "_offset_x") + py_cursor.offset_y = S.obs_data_get_int(settings, "_offset_y") + + py_cursor.width = S.obs_data_get_int(settings, "_width") + py_cursor.width = S.obs_data_get_int(settings, "_height") + py_cursor.browser_source_name = S.obs_data_get_string(settings, "browser") + py_cursor.is_update_browser = S.obs_data_get_bool(settings, "_is_update_browser") def script_properties(): - props = obs.obs_properties_create() - number = obs.obs_properties_add_int( + props = S.obs_properties_create() + number = S.obs_properties_add_int( props, "_refresh_rate", "Refresh rate (ms)", 15, 300, 5 ) ## i am only winging this so please forgive me - offsetx = obs.obs_properties_add_int( - props, "_offset_x", "Offset X", -5000, 5000, 1 - ) - offsety = obs.obs_properties_add_int( - props, "_offset_y", "Offset Y", -5000, 5000, 1 - ) - - p1 = obs.obs_properties_add_list( + S.obs_properties_add_int(props, "_offset_x", "Offset X", -5000, 5000, 1) + S.obs_properties_add_int(props, "_offset_y", "Offset Y", -5000, 5000, 1) + + S.obs_properties_add_int(props, "_width", "base width", 1, 99999, 1) + S.obs_properties_add_int(props, "_height", "base height", 1, 99999, 1) + + p1 = S.obs_properties_add_list( props, "source", "Select cursor source", - obs.OBS_COMBO_TYPE_EDITABLE, - obs.OBS_COMBO_FORMAT_STRING, + S.OBS_COMBO_TYPE_EDITABLE, + S.OBS_COMBO_FORMAT_STRING, ) - p2 = obs.obs_properties_add_list( + p2 = S.obs_properties_add_list( props, "target", "Select target window", - obs.OBS_COMBO_TYPE_EDITABLE, - obs.OBS_COMBO_FORMAT_STRING, + S.OBS_COMBO_TYPE_EDITABLE, + S.OBS_COMBO_FORMAT_STRING, ) - sources = obs.obs_enum_sources() + + S.obs_properties_add_bool(props, "_is_update_browser", "Use browser source") + p3 = S.obs_properties_add_list( + props, + "browser", + "Select browser source", + S.OBS_COMBO_TYPE_EDITABLE, + S.OBS_COMBO_FORMAT_STRING, + ) + + sources = S.obs_enum_sources() if sources is not None: - ## property 1 for image source for source in sources: - source_id = obs.obs_source_get_unversioned_id(source) - name = obs.obs_source_get_name(source) - obs.obs_property_list_add_string(p1, name, name) - ## property 2 for target window + source_id = S.obs_source_get_unversioned_id(source) + name = S.obs_source_get_name(source) + S.obs_property_list_add_string(p1, name, name) for target in sources: - source_id = obs.obs_source_get_unversioned_id(target) - name = obs.obs_source_get_name(target) - obs.obs_property_list_add_string(p2, name, name) - - obs.source_list_release(sources) - obs.obs_properties_add_button(props, "button", "Stop", stop_pressed) - obs.obs_properties_add_button(props, "button2", "Start", start_pressed) - obs.obs_properties_add_bool(props, "bool_yn", "Update crop") + source_id = S.obs_source_get_unversioned_id(target) + name = S.obs_source_get_name(target) + S.obs_property_list_add_string(p2, name, name) + for b in sources: + source_id = S.obs_source_get_unversioned_id(b) + if source_id == "browser_source": + name = S.obs_source_get_name(b) + S.obs_property_list_add_string(p3, name, name) + + S.source_list_release(sources) + S.obs_properties_add_button(props, "button", "Stop", stop_pressed) + S.obs_properties_add_button(props, "button2", "Start", start_pressed) return props