Font Animation Effects

User
<script>
var maxDist;
var mouse = { x: 0, y: 0 };
var cursor = {
    x: window.innerWidth,
    y: window.innerHeight
};

Math.dist = function(a, b) {
    var dx = b.x - a.x;
    var dy = b.y - a.y;
    return Math.sqrt(Math.pow(dx, 2), Math.pow(dy, 2));
}

window.addEventListener("mousemove", function(e) {
    cursor.x = e.clientX;
    cursor.y = e.clientY;
});

window.addEventListener("touchmove", function(e) {
    var t = e.touches[0];
    cursor.x = t.clientX;
    cursor.y = t.clientY;
}, {
    passive: false
});

var Char = function(container, char) {
    var span = document.createElement("span");
    span.setAttribute('data-char', char);
    span.innerText = char;
    container.appendChild(span);
    this.getDist = function() {
        this.pos = span.getBoundingClientRect();
        return Math.dist(mouse, {
            x: this.pos.x + (this.pos.width / 1.75),
            y: this.pos.y
        });
    }
    this.getAttr = function(dist, min, max) {
        var wght = max - Math.abs((max * dist / maxDist));
        return Math.max(min, wght + min);
    }
    this.update = function(args) {
        var dist = this.getDist();
        this.wdth = args.wdth ? ~~this.getAttr(dist, 5, 200) : 100;
        this.wght = args.wght ? ~~this.getAttr(dist, 100, 800) : 400;
        this.alpha = args.alpha ? this.getAttr(dist, 0, 1).toFixed(2) : 1;
        this.ital = args.ital ? this.getAttr(dist, 0, 1).toFixed(2) : 0;
        this.draw();
    }
    this.draw = function() {
        var style = "";
        style += "opacity: " + this.alpha + ";";
        style += "font-variation-settings: 'wght' " + this.wght + ", 'wdth' " + this.wdth + ", 'ital' " + this.ital + ";";
        span.style = style;
    }
    return this;
}

var VFont = function() {
    this.scale = false;
    this.flex = true;
    this.alpha = false;
    this.stroke = false;
    this.width = true;
    this.weight = true;
    this.italic = true;
    var title, str, chars = [];

    this.init = function() {
        title = document.getElementById("title");
        str = title.innerText;
        title.innerHTML = "";
        for (var i = 0; i < str.length; i++) {
            var _char = new Char(title, str[i]);
            chars.push(_char);
        }
        this.set();
        window.addEventListener("resize", this.setSize.bind(this));
    }

    this.set = function() {
        title.className = "";
        title.className += this.flex ? " flex variable-font-content " : "";
        title.className += this.stroke ? " stroke" : "";
        this.setSize();
    }

    this.setSize = function() {
        var mainDiv = document.getElementById("fit"); // assuming the outer div has an id "main"
        var fontSize = mainDiv.clientWidth / (str.length / 2);
        title.style = "font-size: " + fontSize + "px;";
        if (this.scale) {
            var scaleY = (window.innerHeight / title.getBoundingClientRect().height).toFixed(2);
            var lineHeight = scaleY * 0.8;
            title.style = "font-size: " + fontSize + "px; transform: scale(1," + scaleY + "); line-height: " + lineHeight + "em;"
        }
    }

    this.animate = function() {
        mouse.x += (cursor.x - mouse.x) / 20;
        mouse.y += (cursor.y - mouse.y) / 20;
        requestAnimationFrame(this.animate.bind(this));
        this.render();
    }

    this.render = function() {
        maxDist = title.getBoundingClientRect().width / 2;
        for (var i = 0; i < chars.length; i++) {
            chars[i].update({
                wght: this.weight,
                wdth: this.width,
                ital: this.italic,
                alpha: this.alpha
            });
        }
    }
    this.init();
    this.animate();
    return this;
}

var txt = new VFont();

// Start animation
txt.animate();
</script>

Make above for multiple title class

Assistant
To make your variable font animation work for **multiple elements with the class `title`**, you'll need to adjust the logic so that it loops over all `.title` elements, initializes characters for each, and animates them accordingly.

Here’s a modified version of your script to support **multiple `.title` elements**:

Assistant
{
"name": "variable_font_titles",
"type": "code/javascript",
"content": "

<script>
nvar maxDist;nvar mouse = { x: 0, y: 0 };nvar cursor = {n    x: window.innerWidth,n    y: window.innerHeightn};nnMath.dist = function(a, b) {n    var dx = b.x - a.x;n    var dy = b.y - a.y;n    return Math.sqrt(dx * dx + dy * dy);n}nnwindow.addEventListener("mousemove", function(e) {n    cursor.x = e.clientX;n    cursor.y = e.clientY;n});nnwindow.addEventListener("touchmove", function(e) {n    var t = e.touches[0];n    cursor.x = t.clientX;n    cursor.y = t.clientY;n}, {n    passive: falsen});nnvar Char = function(container, char) {n    var span = document.createElement("span");n    span.setAttribute('data-char', char);n    span.innerText = char;n    container.appendChild(span);n    this.getDist = function() {n        this.pos = span.getBoundingClientRect();n        return Math.dist(mouse, {n            x: this.pos.x + (this.pos.width / 1.75),n            y: this.pos.yn        });n    }n    this.getAttr = function(dist, min, max) {n        var wght = max - Math.abs((max * dist / maxDist));n        return Math.max(min, wght + min);n    }n    this.update = function(args) {n        var dist = this.getDist();n        this.wdth = args.wdth ? ~~this.getAttr(dist, 5, 200) : 100;n        this.wght = args.wght ? ~~this.getAttr(dist, 100, 800) : 400;n        this.alpha = args.alpha ? this.getAttr(dist, 0, 1).toFixed(2) : 1;n        this.ital = args.ital ? this.getAttr(dist, 0, 1).toFixed(2) : 0;n        this.draw();n    }n    this.draw = function() {n        var style = "";n        style += "opacity: " + this.alpha + ";";n        style += "font-variation-settings: 'wght' " + this.wght + ", 'wdth' " + this.wdth + ", 'ital' " + this.ital + ";";n        span.style = style;n    }n    return this;n}nnvar VFont = function(element) {n    this.scale = false;n    this.flex = true;n    this.alpha = false;n    this.stroke = false;n    this.width = true;n    this.weight = true;n    this.italic = true;n    var title = element;n    var str = title.innerText;n    var chars = [];nn    this.init = function() {n        title.innerHTML = "";n        for (var i = 0; i < str.length; i++) {n            var _char = new Char(title, str[i]);n            chars.push(_char);n        }n        this.set();n        window.addEventListener("resize", this.setSize.bind(this));n    }nn    this.set = function() {n        title.className += this.flex ? " flex variable-font-content " : "";n        title.className += this.stroke ? " stroke" : "";n        this.setSize();n    }nn    this.setSize = function() {n        var mainDiv = title.parentElement;n        var fontSize = mainDiv.clientWidth / (str.length / 2);n        title.style.fontSize = fontSize + "px";n        if (this.scale) {n            var scaleY = (window.innerHeight / title.getBoundingClientRect().height).toFixed(2);n            var lineHeight = scaleY * 0.8;n            title.style.transform = "scale(1," + scaleY + ")";n            title.style.lineHeight = lineHeight + "em";n        }n    }nn    this.animate = function() {n        mouse.x += (cursor.x - mouse.x) / 20;n        mouse.y += (cursor.y - mouse.y) / 20;n        requestAnimationFrame(this.animate.bind(this));n        this.render();n    }nn    this.render = function() {n        maxDist = title.getBoundingClientRect().width / 2;n        for (var i = 0; i < chars.length; i++) {n            chars[i].update({n                wght: this.weight,n                wdth: this.width,n                ital: this.italic,n                alpha: this.alphan            });n        }n    }nn    this.init();n    return this;n}nn// Initialize for all elements with class "title"nvar titles = document.querySelectorAll(".title");nvar fontObjects = [];ntitles.forEach(function(el) {n    var txt = new VFont(el);n    fontObjects.push(txt);n});nn// Unified animation loop for allnfunction animateAll() {n    mouse.x += (cursor.x - mouse.x) / 20;n    mouse.y += (cursor.y - mouse.y) / 20;n    requestAnimationFrame(animateAll);n    fontObjects.forEach(function(font) {n        font.render();n    });n}nanimateAll();n
</script>

"
}

Assistant
✅ Now, every element with the class `.title` will respond to mouse movement and animate the font properties accordingly. Just make sure each `.title` is wrapped in a container (like a “) for proper font sizing. Let me know if you'd like to add scroll or click-based animations too!
User
title font now becomess smaller I want to view same size
Assistant
Got it! The reason the font appears smaller now is because of this part in your code:

var fontSize = mainDiv.clientWidth / (str.length / 2);

This dynamically adjusts the font size based on the container width and character count — useful for responsive scaling, but it reduces the size if you have many characters.

### ✅ To keep a **consistent font size** (e.g., `80px` across all titles), do this:

#### 🔧 Change the `setSize()` method inside the `VFont` function:

Replace this block:

var fontSize = mainDiv.clientWidth / (str.length / 2);
title.style.fontSize = fontSize + "px";

With:

title.style.fontSize = "80px"; // Or whatever fixed size you want

### 🛠️ Updated `setSize()` function:
Here's the clean version you can use:

this.setSize = function() {
    title.style.fontSize = "80px"; // Set a fixed size
    if (this.scale) {
        var scaleY = (window.innerHeight / title.getBoundingClientRect().height).toFixed(2);
        var lineHeight = scaleY * 0.8;
        title.style.transform = "scale(1," + scaleY + ")";
        title.style.lineHeight = lineHeight + "em";
    }
}

Let me know if you want to make this font size responsive but with a minimum/maximum limit instead.

User
NO If we have different elementer widget on same page then previuse elementor instance title font size become smaller
Assistant
Thanks for the clarification — that makes sense now.

You're seeing the **previous Elementor widget's title font size shrink** because the shared logic (`maxDist`, `mouse`, `cursor`, etc.) is **global** and being reused across multiple widget instances. When a new `VFont` instance is initialized, it **overwrites** some shared state like `maxDist` and `title`, which affects the already-initialized elements.

### ✅ Solution: Make each title's `maxDist` and `title` **instance-specific**, not global.

We'll:

– Remove global `maxDist` and `title` variables.
– Move `maxDist` into the `VFont` instance.
– Use `this.title` instead of global `title` in the methods.

### ✏️ Updated Fix (Minimal Changes)
Update this inside your `VFont` function:

var VFont = function(element) {
    this.title = element; // use instance-specific title
    var str = this.title.innerText;
    var chars = [];
    this.maxDist = 0;

    // update init
    this.init = function() {
        this.title.innerHTML = "";
        for (var i = 0; i < str.length; i++) {
            var _char = new Char(this.title, str[i]);
            chars.push(_char);
        }
        this.set();
        window.addEventListener("resize", this.setSize.bind(this));
    }

    this.set = function() {
        this.title.className += this.flex ? " flex variable-font-content " : "";
        this.title.className += this.stroke ? " stroke" : "";
        this.setSize();
    }

    this.setSize = function() {
        this.title.style.fontSize = "80px"; // Or keep dynamic if needed
        if (this.scale) {
            var scaleY = (window.innerHeight / this.title.getBoundingClientRect().height).toFixed(2);
            var lineHeight = scaleY * 0.8;
            this.title.style.transform = "scale(1," + scaleY + ")";
            this.title.style.lineHeight = lineHeight + "em";
        }
    }

    this.render = function() {
        this.maxDist = this.title.getBoundingClientRect().width / 2;
        for (var i = 0; i < chars.length; i++) {
            chars[i].update({
                wght: this.weight,
                wdth: this.width,
                ital: this.italic,
                alpha: this.alpha,
                maxDist: this.maxDist
            });
        }
    }

    this.init();
    return this;
}

Then update the `Char.prototype.update()` logic to accept `maxDist` from arguments:

this.update = function(args) {
    var dist = this.getDist();
    var maxDist = args.maxDist;
    this.wdth = args.wdth ? ~~this.getAttr(dist, 5, 200, maxDist) : 100;
    this.wght = args.wght ? ~~this.getAttr(dist, 100, 800, maxDist) : 400;
    this.alpha = args.alpha ? this.getAttr(dist, 0, 1, maxDist).toFixed(2) : 1;
    this.ital = args.ital ? this.getAttr(dist, 0, 1, maxDist).toFixed(2) : 0;
    this.draw();
}

And modify the `getAttr()` method:

this.getAttr = function(dist, min, max, maxDist) {
    var wght = max - Math.abs((max * dist / maxDist));
    return Math.max(min, wght + min);
}

This will keep **each widget's font animation scoped to itself**, and ensure other titles retain their correct size and animation behavior even if multiple Elementor widgets are on the page.

Let me know if you'd like to isolate `mouse` and `cursor` per widget too — or keep those shared for hover interactions across all titles.

User
But now animation stopped working
Assistant
Thanks for pointing that out! You're absolutely right — the animation stops working because I introduced an oversight in the update: `maxDist` is now passed per character, but `Char` was not fully adapted to use it properly.

Let me walk you through the correct fix so **each Elementor widget has its own animation without breaking it.**

### ✅ Clean Fix – Keep Animation + Prevent Cross-widget Conflict

We’ll:
1. Scope everything inside each `VFont` instance.
2. Keep `mouse` and `cursor` global (since interaction is global).
3. **Avoid overwriting shared values** like `maxDist` across widgets.
4. Ensure `requestAnimationFrame` calls don’t conflict.

### ✅ Final Working Code Snippet (Fix applied)

Update your `Char` class first to accept and use `maxDist` from its parent instance:

this.getAttr = function(dist, min, max, maxDist) {
    var wght = max - Math.abs((max * dist / maxDist));
    return Math.max(min, wght + min);
}
this.update = function(args, maxDist) {
    var dist = this.getDist();
    this.wdth = args.wdth ? ~~this.getAttr(dist, 5, 200, maxDist) : 100;
    this.wght = args.wght ? ~~this.getAttr(dist, 100, 800, maxDist) : 400;
    this.alpha = args.alpha ? this.getAttr(dist, 0, 1, maxDist).toFixed(2) : 1;
    this.ital = args.ital ? this.getAttr(dist, 0, 1, maxDist).toFixed(2) : 0;
    this.draw();
}

Then inside `VFont`, use `this.maxDist` properly:

this.render = function() {
    this.maxDist = this.title.getBoundingClientRect().width / 2;
    for (var i = 0; i < chars.length; i++) {
        chars[i].update({
            wght: this.weight,
            wdth: this.width,
            ital: this.italic,
            alpha: this.alpha
        }, this.maxDist);
    }
}

### ✅ Make Sure You Have Global Mouse Tracker + Unified Animation Loop

At the bottom of your script:

// Global mouse tracker
window.addEventListener("mousemove", function(e) {
    cursor.x = e.clientX;
    cursor.y = e.clientY;
});

window.addEventListener("touchmove", function(e) {
    var t = e.touches[0];
    cursor.x = t.clientX;
    cursor.y = t.clientY;
}, {
    passive: false
});

// All VFont instances
var titles = document.querySelectorAll(".title");
var fontObjects = [];
titles.forEach(function(el) {
    fontObjects.push(new VFont(el));
});

// Shared animation loop
function animateAll() {
    mouse.x += (cursor.x - mouse.x) / 20;
    mouse.y += (cursor.y - mouse.y) / 20;
    requestAnimationFrame(animateAll);
    fontObjects.forEach(font => font.render());
}
animateAll();

### ✅ Bonus: Font Size Fix

If you want to preserve the Elementor-defined font size (e.g., from the builder), you can remove `fontSize` logic entirely from `setSize()`:

this.setSize = function() {
    // don't touch font size, let Elementor control it
}

Let me know if you'd like to add scroll or viewport detection so the effect only runs when a title is visible!

Leave a Comment

Your email address will not be published. Required fields are marked *