Font Animation Effects
March 27, 2025 · AI Conversations
<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
Here’s a modified version of your script to support **multiple `.title` elements**:
"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>
"
}
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.
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.
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!