diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f5d354 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +local/* diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..26ba001 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Anar Bastanov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LivelyInfo.json b/LivelyInfo.json new file mode 100644 index 0000000..2291ed2 --- /dev/null +++ b/LivelyInfo.json @@ -0,0 +1,12 @@ +{ + "AppVersion": "1.0.0.0", + "Title": "Enchanted Cursor", + "Thumbnail": "res/thumbnail.png", + "Preview": "res/preview.gif", + "Desc": "Rainbow cursor effect using HTML5 Canvas.", + "Author": "Anar Bastanov", + "License": "MIT", + "Contact": "https://github.com/anar-bastanov/enchanted-cursor-wallpaper", + "Type": 1, + "FileName": "src/index.html" +} diff --git a/LivelyProperties.json b/LivelyProperties.json new file mode 100644 index 0000000..24f664a --- /dev/null +++ b/LivelyProperties.json @@ -0,0 +1,99 @@ +{ + "labelCursor": { + "type": "label", + "value": "Cursor" + }, + "radius": { + "text": "Radius", + "type": "slider", + "value": 5, + "min": 1, + "max": 20 + }, + "rainbowSpeed": { + "text": "Rainbow Speed", + "type": "slider", + "value": 30, + "min": 1, + "max": 100 + }, + "randomBrightness": { + "text": "Random Brightness", + "type": "slider", + "value": 70, + "min": 0, + "max": 100 + }, + "distortion": { + "text": "Distortion", + "type": "slider", + "value": 40, + "min": 0, + "max": 100 + }, + "labelWindow": { + "type": "label", + "value": "Window" + }, + "fadeSpeed": { + "text": "Fade Speed", + "type": "slider", + "value": 1, + "min": 1, + "max": 100 + }, + "backgroundNoise": { + "text": "Background Noise", + "type": "slider", + "value": 25, + "min": 0, + "max": 100 + }, + "syncNoiseColor": { + "text": "Sync Noise Color", + "type": "checkbox", + "value": false + }, + "screenSaturation": { + "text": "Screen Saturation", + "type": "slider", + "value": 50, + "min": 0, + "max": 100 + }, + "labelText": { + "type": "label", + "value": "Text" + }, + "characters": { + "text": "Characters", + "type": "textbox", + "value": "ᔑʖᓵ↸ᒷ⎓⊣⍑╎⋮ꖌꖎᒲリ𝙹!¡ᑑ∷ᓭℸ⚍⍊∴/||⨅" + }, + "fontSize": { + "text": "Font Size", + "type": "slider", + "value": 12, + "min": 8, + "max": 30 + }, + "fontName": { + "text": "Font Name", + "type": "dropdown", + "value": 11, + "items": [ + "Arial", + "Verdana", + "Times New Roman", + "Georgia", + "Courier New", + "Lucida Console", + "Trebuchet MS", + "Tahoma", + "Palatino Linotype", + "Impact", + "Comic Sans MS", + "Consolas" + ] + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba05ac2 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Enchanted Cursor +![Demo](res/preview.gif?raw=true "video") + +Desktop wallpaper with an enchanted cursor effect. +Use with [Lively Wallpaper](https://github.com/rocksdanister/lively) software. + +## Customization +The following parameters can be adjusted from a settings menu within Lively: +* Effect radius +* Rainbow color speed +* Random brightness +* Distortion in circle area +* Fade speed +* Background noise intensity +* Sync background noise color +* Screen saturation +* Displayed character set +* Font size +* Font name + +## Download +[Download](https://github.com/anar-bastanov/enchanted-cursor-wallpaper/releases/download/v1.0.0/enchanted-cursor-wallpaper.zip) + +Drag & drop the file into Lively window to install. diff --git a/res/preview.gif b/res/preview.gif new file mode 100644 index 0000000..6a9220e Binary files /dev/null and b/res/preview.gif differ diff --git a/res/thumbnail.png b/res/thumbnail.png new file mode 100644 index 0000000..718446b Binary files /dev/null and b/res/thumbnail.png differ diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..f0b194a --- /dev/null +++ b/src/index.html @@ -0,0 +1,12 @@ + + + + + + Enchanted Cursor + + + + + + diff --git a/src/script.js b/src/script.js new file mode 100644 index 0000000..fccb770 --- /dev/null +++ b/src/script.js @@ -0,0 +1,229 @@ +const config = { + RADIUS: 5, // int [1, 20] + RAINBOW_SPEED: 0.3, // float (0, 1] + RANDOM_BRIGHTNESS: 0.7, // float [0, 1] + DISTORTION: 2, // float [0, 5] + FADE_DECAY: 50, // int [1, 50] + BACKGROUND_NOISE: 0.25, // float [0, 1] + SYNC_NOISE_COLOR: false, // bool + SCREEN_SATURATION: 0.5, // float [0, 1] + CHARACTERS: [..."ᔑʖᓵ↸ᒷ⎓⊣⍑╎⋮ꖌꖎᒲリ𝙹!¡ᑑ∷ᓭℸ⚍⍊∴/||⨅"], // string [1, 64] + FONT_SIZE: 12, // int [8, 30] + FONT_NAME: "Consolas, monospace", + __AVAILABLE_FONTS: [ + "Arial, sans-serif", + "Verdana, sans-serif", + "Times New Roman, serif", + "Georgia, serif", + "Courier New, monospace", + "Lucida Console, monospace", + "Trebuchet MS, sans-serif", + "Tahoma, sans-serif", + "Palatino Linotype, serif", + "Impact, sans-serif", + "Comic Sans MS, cursive, sans-serif", + "Consolas, monospace", + ] +}; + +//# Setup + +const canvas = document.getElementById("c"); +const context = canvas.getContext("2d"); + +canvas.height = window.innerHeight; +canvas.width = window.innerWidth; + +context.font = config.FONT_SIZE + "px " + config.FONT_NAME; // for debugging on a browser + +let hue = 0; +let mouseXOld = -1; +let mouseYOld = -1; + +//# Main Functions + +function alignPosition(pos) { + const fontSize = config.FONT_SIZE; + return Math.floor(pos / fontSize) * fontSize; +} + +function getRandomCharacter() { + const characters = config.CHARACTERS; + return characters[Math.floor(Math.random() * characters.length)]; +} + +function drawCharacter(char, posX, posY, hue, alpha = 1.0) { + const fontSize = config.FONT_SIZE; + + context.fillStyle = "rgba(10,10,10, 1.0)"; + context.fillRect(posX, posY - fontSize, fontSize, fontSize); + + const r = Math.floor(127 * Math.sin(hue + (0 / 3) * Math.PI) + 128); + const g = Math.floor(127 * Math.sin(hue + (2 / 3) * Math.PI) + 128); + const b = Math.floor(127 * Math.sin(hue + (4 / 3) * Math.PI) + 128); + + context.fillStyle = `rgba(${r},${g},${b}, ${alpha})`; + context.fillText(char, posX, posY); +} + +function drawCursorEffect(mouseX, mouseY) { + const { + RADIUS: radius, + RAINBOW_SPEED: speed, + RANDOM_BRIGHTNESS: brightness, + DISTORTION: distortion, + FONT_SIZE: fontSize + } = config; + const cellX = alignPosition(mouseX); + const cellY = alignPosition(mouseY); + + for (let x = -radius + 1; x < radius; ++x) { + for (let y = -radius + 1; y < radius; ++y) { + if (Math.sqrt(x * x + y * y) >= radius - 0.5) + continue; + + const distance = Math.sqrt(x * x + y * y) / radius; + + if (Math.random() > Math.pow(1 - distance, distortion)) + continue; + + const char = getRandomCharacter(); + const posX = cellX + x * fontSize; + const posY = cellY + y * fontSize; + const alpha = 1.0 - brightness * Math.random(); + + drawCharacter(char, posX, posY, hue * speed, alpha); + } + } +} + +function drawCursorEffectInLine(mouseXFrom, mouseYFrom, mouseXTo, mouseYTo) { + const diffX = mouseXTo - mouseXFrom; + const diffY = mouseYTo - mouseYFrom; + const length = config.FONT_SIZE * config.RADIUS / 1.5; + const distance = Math.sqrt(diffX * diffX + diffY * diffY); + const lengthX = length * diffX / distance; + const lengthY = length * diffY / distance; + const maxHuePeriod = 2 * Math.PI / config.RAINBOW_SPEED; + + let i = distance; + do { + hue = (hue + min(length, i) / 75) % maxHuePeriod; + drawCursorEffect(mouseXTo, mouseYTo); + mouseXTo -= lengthX; + mouseYTo -= lengthY; + i -= length; + } + while (i > 0); +} + +function drawNoiseEffect() { + const { + RAINBOW_SPEED: speed, + BACKGROUND_NOISE: noise, + SYNC_NOISE_COLOR: syncColor, + FONT_SIZE: fontSize + } = config; + const count = Math.sqrt(canvas.width * canvas.height) / fontSize * (0.2 * noise); + + for (let i = 0; i < count; ++i) { + const char = getRandomCharacter(); + const posX = alignPosition(Math.random() * canvas.width); + const posY = alignPosition(Math.random() * canvas.height); + const rhue = syncColor ? hue * speed : Math.random() * 2 * Math.PI; + + drawCharacter(char, posX, posY, rhue); + } +} + +function fadeScreen() { + context.fillStyle = "rgba(10,10,10, 0.05)"; + context.fillRect(0, 0, canvas.width, canvas.height); + context.fillStyle = "rgba(0,0,0, 0.05)"; + context.fillRect(0, 0, canvas.width, canvas.height); +} + +//# Events + +window.onresize = () => { + location.reload(); +} + +canvas.onmouseleave = () => { + mouseXOld = -1; + mouseYOld = -1; +} + +let fadeIntervalId = setInterval(fadeScreen, config.FADE_DECAY); + +setInterval(drawNoiseEffect, 50); + +document.onmousemove = (event) => { + const mouseX = event.x; + const mouseY = event.y; + + if (mouseXOld < 0 || mouseYOld < 0) + drawCursorEffect(mouseX, mouseY); + else + drawCursorEffectInLine(mouseXOld, mouseYOld, mouseX, mouseY); + + mouseXOld = mouseX; + mouseYOld = mouseY; +} + +//# Lively API + +function livelyPropertyListener(name, val) { + switch (name) { + case "radius": + config.RADIUS = val; + break; + case "rainbowSpeed": + config.RAINBOW_SPEED = val / 100; + break; + case "randomBrightness": + config.RANDOM_BRIGHTNESS = val / 100; + break; + case "distortion": + config.DISTORTION = val / 20; + break; + case "fadeSpeed": + val = mapRange(val, 1, 100, 50, 1); + config.FADE_DECAY = val; + clearInterval(fadeIntervalId); + fadeIntervalId = setInterval(fadeScreen, val); + break; + case "backgroundNoise": + config.BACKGROUND_NOISE = val / 100; + break; + case "syncNoiseColor": + config.SYNC_NOISE_COLOR = val; + break; + case "screenSaturation": + config.SCREEN_SATURATION = val / 100; + canvas.style.filter = `saturate(${val + 100}%) brightness(${val + 100}%)`; + break; + case "characters": + config.CHARACTERS = val.length == 0 ? ['?'] : [...val.slice(0, 64)]; + break; + case "fontSize": + config.FONT_SIZE = val; + context.font = val + "px " + config.FONT_NAME; + break; + case "fontName": + val = config.__AVAILABLE_FONTS[val]; + config.FONT_NAME = val; + context.font = config.FONT_SIZE + "px " + val; + break; + } +} + +//# Utility Functions + +function mapRange(value, inMin, inMax, outMin, outMax) { + return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin; +} + +function min(x, y) { + return x < y ? x : y; +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..da34060 --- /dev/null +++ b/src/style.css @@ -0,0 +1,16 @@ +* { + margin: 0; + padding: 0; +} + +body { + background: rgba(10, 10, 10, 1.0); +} + +canvas { + display: block; +} + +#c { + filter: saturate(1.5) brightness(150%); +}