Animating HTML objects that move when text is modified / changed - javascript

I have this code:
<span><img id="user" src="http://placehold.it/100x100" alt="img"></span>
<span><h2 id="textToChange">text here</h2></span>
When I change #textToChange the #user moves automatically because the text changes and because it's a span it moves it. I would like to animate #user so it will move in a linear animation instead of just getting to the x value.
Is something like to possible to achieve or am I crazy? Thanks.

This is an interesting little problem. Let me break it down into steps:
Find the image's current position
Find where it needs to move to
Move it
Change the text
Now, the hard parts are steps 2 and 3.
For step 2, you have to calculate the length of the new text. This is tricky because there are no built-in functions to tell you how wide text will be with a given set of styles. You pretty much have to create a duplicate element and measure it instead.
For step 3, you have to move the element without causing a jump before or after the text changes. My way of doing this is to use position: absolute and set left to the current position (thus eliminating any jerking there). I then transition to the correct position using transform (doing a little math to account for the current position), for performance. At the end of the transition, take away the style attribute and change the text.
One other thing to watch out for is the text jumping around when the image becomes position: absolute. For simplicity, I put the entire line in a display: flex container. If you don't want to use flex, you can use inline-block or block on the text and adjust the padding/height so it will keep the proper amount of space.
Here's what I came up with (also on JSFiddle):
var $img = document.getElementById('user');
var $text = document.getElementById('textToChange');
var $estimator = document.getElementById('estimator');
var extraWidth = $img.offsetLeft - $text.offsetWidth;
function estimate(text) {
$estimator.textContent = text;
var width = $estimator.offsetWidth;
$estimator.textContent = '';
return width;
}
document.getElementById('change-text')
.addEventListener('click', function() {
var newText = randomText();
var left = $img.offsetLeft;
$img.style.position = 'absolute';
$img.style.left = left + 'px';
$img.style.transition = 'transform linear 1s';
$img.style.transform = 'translateX(0)';
window.requestAnimationFrame(function() {
$img.style.transform = 'translateX(' + (extraWidth + estimate(newText) - left) + 'px)';
window.setTimeout(function() {
$text.textContent = newText;
$img.removeAttribute('style');
}, 1000);
});
});
// For testing
function randomText() {
var length = Math.floor(Math.random() * 43) + 3;
return 'Lorem ipsum dolor sit amet portris noc tefweep'.slice(0, length);
}
h2 {
position: relative;
height: 100px;
display: flex;
align-items: flex-end;
}
/* For measuring text width. I don't want it to be seen. */
.not-shown {
visibility: hidden;
color: transparent;
position: absolute;
z-index: -1;
}
<h2>
<span id="textToChange">text here</span>
<img id="user" src="http://placehold.it/100x100" alt="img">
</h2>
<h2 class="not-shown"><span id="estimator"></span></h2>
<button id="change-text">Change text</button>
Note that this does not work well if the text goes to multiple lines. I chose not to worry about that scenario.

Related

Dynamically adjust a div's css zoom to fit container's size

Suppose such html code:
<editor>
<tree></tree>
</editor>
In my application, the tree is used to store user's input, for example:
'123'
'1111111111111111111111111111111111111111111111111111111'
So overflow is possible if text is too long.
I'd like to apply a css 'zoom' style to tree, to ensure it's size is smaller than editor.
How can I calculate the prefect zoom, using JavaScript?
You can effectively just scale it down step by step until it fits in the container.
Effectively this is:
Styling the elements so they will naturally overflow
Stepping down the scale 5% at a time
Stopping once the child element is smaller than it's parent
function calcSize() {
// The elements we need to use and our current scale
var editor = document.getElementById("editor")
var tree = document.getElementById("tree")
var scale = 1;
// Reset the initial scale and style incase we are resizing the page
tree.classList.add("loading");
tree.style.transform = "scale(1)";
// Loop until the scale is small enough to fit it's container
while (
(editor.getBoundingClientRect().width <
tree.getBoundingClientRect().width) &&
(scale > 0) // This is just incase even at 0.05 scale it doesn't fit, at which point this would cause an infinate loop if we didn't have this check
) {
// Reduce the scale
scale -= 0.05;
// Apply the new scale
tree.style.transform = "scale(" + scale + ")";
}
// Display the final result
tree.classList.remove("loading");
console.log("Final scale: " + Math.round(scale * 100) / 100)
}
// Run on load and on resize
calcSize();
window.addEventListener("resize", calcSize);
#editor {
display: block;
max-width: 50%;
font-size: 20px;
border: 1px solid #000;
overflow: visible;
}
#tree {
display: inline-block;
white-space: nowrap;
/* This is important as the default scale will be relative to the overflowed size */
transform-origin: 0 50%;
}
#tree.loading {
opacity: 0;
}
<editor id="editor">
<tree id="tree" class="loading">This is some overflowing text This is some overflowing text.</tree>
</editor>
(Try viewing the snippet in fullscreen and resizing the window to see it in effect)

How to restart updated animation after dynamically changing keyframes?

I'm trying to create a marquee (yes, I've done LOTS of searching on that topic first) using animated text-indent. I prefer this solution over others I've tried, like using translation 100%, which causes text to leak out beyond the boundaries of my marquee.
I've been trying to follow this example here: https://www.jonathan-petitcolas.com/2013/05/06/simulate-marquee-tag-in-css-and-javascript.html
...which I've updated a bit, doing it in TypeScript, using API updates (appendRule instead of insertRule) and dropping concerns about old browser support.
The problem is that the animation restarts using the old keyframe rules -- the step described by the comment "re-assign the animation (to make it run)" doesn't work.
I've looked at what's going on in a debugger, and the rules are definitely being changed -- old rules deleted, new rules added. But it's as if the old rules are cached somewhere, and they aren't being cleared out.
Here's my current CSS:
#marquee {
position: fixed;
left: 0;
right: 170px;
bottom: 0;
background-color: midnightblue;
font-size: 14px;
padding: 2px 1em;
overflow: hidden;
white-space: nowrap;
animation: none;
}
#marquee:hover {
animation-play-state: paused;
}
#keyframes marquee-0 {
0% {
text-indent: 450px;
}
100% {
text-indent: -500px;
}
}
And the relevant section of my TypeScript:
function updateMarqueeAnimation() {
const marqueeRule = getKeyframesRule('marquee-0');
if (!marqueeRule)
return;
marquee.css('animation', 'unset');
const element = marquee[0];
const textWidth = getTextWidth(marquee.text(), element);
const padding = Number(window.getComputedStyle(element).getPropertyValue('padding-left').replace('px', '')) +
Number(window.getComputedStyle(element).getPropertyValue('padding-right').replace('px', ''));
const offsetWidth = element.offsetWidth;
if (textWidth + padding <= offsetWidth)
return;
marqueeRule.deleteRule('0%');
marqueeRule.deleteRule('100%');
marqueeRule.appendRule('0% { text-indent: ' + offsetWidth + 'px; }');
marqueeRule.appendRule('100% { text-indent: -' + textWidth + 'px; }');
setTimeout(() => marquee.css('animation', 'marquee-0 15s linear infinite'));
}
I've tried a number of tricks so far to get around this problem, including things like cloning the marquee element and replacing it with its own clone, and none of that has helped -- the animation continues to run as if the original stylesheet values are in effect, so the scrolling of the marquee doesn't adapt to different widths of text.
The next thing I'll probably try is dynamically creating new keyframes objects instead of editing the rules inside of an existing keyframes object, but that's a messy solution I'd rather avoid if anyone has a better solution.
I found a way to get my marquee working, and it did involved dynamically adding and removing keyframes rules from a stylesheet, but that wasn't as painful or ugly as I thought it might be.
let animationStyleSheet: CSSStyleSheet;
let keyframesIndex = 0;
let lastMarqueeText = '';
function updateMarqueeAnimation(event?: Event) {
const newText = marquee.text();
if (event === null && lastMarqueeText === newText)
return;
lastMarqueeText = newText;
marquee.css('animation', 'none');
const element = marquee[0];
const textWidth = getTextWidth(newText, element);
const padding = Number(window.getComputedStyle(element).getPropertyValue('padding-left').replace('px', '')) +
Number(window.getComputedStyle(element).getPropertyValue('padding-right').replace('px', ''));
const offsetWidth = element.offsetWidth;
if (textWidth + padding <= offsetWidth)
return;
if (!animationStyleSheet) {
$('head').append('<style id="marquee-animations" type="text/css"></style>');
animationStyleSheet = ($('#marquee-animations').get(0) as HTMLStyleElement).sheet as CSSStyleSheet;
}
if (animationStyleSheet.cssRules.length > 0)
animationStyleSheet.deleteRule(0);
const keyframesName = 'marquee-' + keyframesIndex++;
const keyframesRule = `#keyframes ${keyframesName} { 0% { text-indent: ${offsetWidth}px } 100% { text-indent: -${textWidth}px; } }`;
const seconds = (textWidth + offsetWidth) / 100;
animationStyleSheet.insertRule(keyframesRule, 0);
marquee.css('animation', `${keyframesName} ${seconds}s linear infinite`);
}
There's other stuff going on here not needed for a general solution. One thing is that this method is called for two reasons: The window is being resized, or an update to the marquee text has been made. I always want to update when the window is resized, but otherwise I don't want to update the animation if the text hasn't changed, otherwise it could unnecessarily reset when someone is trying to read it.
The other thing is that I don't want text to scroll at all if it happens to fit nicely without scrolling.

Highlighting line of text in div while scrolling

I'm trying to highlight some text in a div, with the highlight being a fixed line in said text. So far I've got a very simple solution that uses two divs, one that houses the text, and the other acting as the highlight, and as you scroll the text, it will pass through the highlight div.
HTML is as follows:
<div id="test">
text...
</div>
<div id="highlight"></div>
CSS is:
#highlight {
position: fixed;
top: 50%;
width: 100%;
background-color: #ccff00;
height: 30px;
opacity: 0.6;
}
#test{
position: absolute;
font-size: 30px;
top: 50%;
}
A demo of it can be found here
I was wondering if anyone knows how to make it so that scrolling the text can be done in a way where as a user scrolls, the next line becomes highlighted. Currently it scrolls normally, so the highlight may miss a line, or not highlight a complete line. Additionally, I was wondering how it would be best to make the text scroll all the way to the bottom. Would adding a margin of the same size as the offset at the top work? Alternative solutions for any of this would be appreciated as well.
Try adding an event listener to the window on scroll. Then calculate the offset by taking the scrollY % line-height and set the highlight top margin to the negative of that value.
JavaScript below:
var highlight = document.querySelector("#highlight");
window.addEventListener('scroll', function(e){
var y = window.scrollY;
var offset = y % 30;
highlight.style.marginTop = - y % 30 + "px";
});
See Working Fiddle
Not sure if this
https://jsfiddle.net/ok0x3apo/6/ is what you're looking for
You can see that I'm remodifying the entered text, to get line by line highlight as page scrolls.
var el = document.getElementById("text"),
content = el.innerHTML.replace(/ |^\s+|\s+$/g,""),
lines = content.split(/\./);
var html = "";
for(var i in lines){
html+="<p class='clear_display' id='id_"+i+"'>"+lines[i]+".</p>";
};
document.getElementById("text").innerHTML=html;
You can make changes to the "clear_display" class on how you prefer to have the text block.
function calledEveryScroll() {
var scrollPosition = $(window).scrollTop();
for(var i in lines){
var currentSection = document.querySelector("#id_"+i+"");
var sectionTop = currentSection.offsetTop;
if (scrollPosition<=0){
$(".clear_display").removeClass('active');
document.querySelector("#id_0").className += " active";
}
if (scrollPosition >= sectionTop-50) {
$(".clear_display").removeClass('active');
if (!$(currentSection).hasClass('active')) {
$(currentSection).addClass('active');
if(previous){
if(currentSection.offsetTop==previous.offsetTop){
$(previous).addClass('active');
}
}
var previous = currentSection;
}
//return false;
}
}
}
function resizing(){
var offset =100;
var bottom = $(window).height()-offset;
$('#text').css('margin-bottom',bottom);
}
This function checks each line when page scrolls.For the scroll to reach the bottom I'm calculating the margin-bottom.Hope it helps.

How to draw lots of lines in html? [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 6 years ago.
Improve this question
I have lots of images in my page and I am looking for a way to draw a line that will connect one image to the other ( it doesn't have to be an arrow, just a normal line. ).For example, let us consider ($) as an image:
$
$
Now how can I connect those 2 images ($) with a line?
Thanks!
Since you seem to be asking about basic JavaScript, HTML, and CSS, here's a simple method using only those. It's nice to understand the math and theory behind doing these kinds of graphical calculations instead of entirely relying on libraries.
Use a HTML div as a line by calculating the distance and angle between two images.
// Get the position of the first image
var imgOnePosition = document.getElementById("one").getBoundingClientRect();
// Get the position of the second image
var imgTwoPosition = document.getElementById("two").getBoundingClientRect();
// Calculate the angle between the two images' positions.
// Math.atan2() returns a value in radians so convert it to degrees as well
var angle = Math.atan2(imgOnePosition.top - imgTwoPosition.top, imgOnePosition.left - imgTwoPosition.left) * (180 / Math.PI);
// Calculate the distance, hopefully you remember this from basic algebra :)
var distance = Math.sqrt(Math.pow(imgOnePosition.top - imgTwoPosition.top, 2) + Math.pow(imgOnePosition.left - imgTwoPosition.left, 2));
// Create a new DIV to represent our line
var line = document.createElement("div");
// Now we style it
line.style.position = "absolute"; // so that we can change left and top
line.style.width = distance + "px";
line.style.height = "2px";
line.style.left = "50%"; // Center the element in its parent
line.style.top = "50%"; // Center the element in its parent
line.style.background = "#000";
line.style.transformOrigin = "0% 50%"; // Rotate around one edge instead of the middle
line.style.transform = "rotate(" + (angle) + "deg)";
// Add the line to the SECOND image's parent element.
// It's the 2nd image instead of 1st because of the order we did the math in calculating the angle
document.getElementById("two").appendChild(line);
body, img {
margin: 0;
padding: 0;
display: block;
}
#container {
position: relative;
background: #ddd;
width: 320px;
height: 240px;
}
.img-container {
position: absolute;
}
<div id="container">
<div id="one" class="img-container" style="left: 50px; top: 100px;" >
<img src="http://imgur.com/8B1rYNY.png" />
</div>
<div id="two" class="img-container" style="left: 150px; top: 190px;" >
<img src="http://imgur.com/8w6LAV6.png" />
</div>
</div>
If you want the line to appear behind the images instead of in front, you could modify their z-index values so they're ordered properly.
Edit: The above works if the images are the exact same size. If they are different sizes, calculate the center point of the images and use that instead of just the top left corner of the getBoundingClientRect().
// Get the position of the first image
var imgOneRect = document.getElementById("one").getBoundingClientRect();
var imgOnePosition = {
left: (imgOneRect.left + imgOneRect.right) / 2,
top: (imgOneRect.top + imgOneRect.bottom) / 2
}
// Get the position of the second image
var imgTwoRect = document.getElementById("two").getBoundingClientRect();
var imgTwoPosition = {
left: (imgTwoRect.left + imgTwoRect.right) / 2,
top: (imgTwoRect.top + imgTwoRect.bottom) / 2
}
div tag: with a background-color, width, height, transform: rotate(50deg) and well positioning properties
SVG tag
PNG image
Canvas

Javascript positioning center style

I wanted to set my second div element indside of my first div element center. I think somehow I managed to center it. But I think I made some mistakes and it seems to me it is not properly centered and also this JavaScript style seems to me bad. Is there any better way doing it? Is my JavaScript code is correct?
FIDDLE
HTML
<div class='first'>
<div class='second'>
</div>
</div>
JavaScript
var first = document.getElementsByClassName('first')[0];
var second = document.getElementsByClassName('second')[0];
var height = first.offsetHeight;
second.style.width = height/2+"px";
second.style.height = height/2+"px";
second.style.marginLeft = height/4+"px";
second.style.marginTop = height/4+"px";
offsetHeight will get the height of the element including borders, clientHeight won't. Instead of:
var height = first.offsetHeight;
Try:
var height = first.clientHeight;
JSFiddle
I've also used top and left with position:absolute for positioning, as this take the element out of the page flow and I assume this is the behaviour you are looking for.
References:
offsetHeight
clientHeight
(Follow the links and take a look at the box-model diagrams)
Reason is drawing round take 3px thats why not positioning but you divide 2.1 that result come that you need.
Check this Demo jsFiddle
JavaScript
var first = document.getElementsByClassName('first')[0];
var second = document.getElementsByClassName('second')[0];
var height = first.offsetHeight;
second.style.width = height/2.1+"px";
second.style.height = height/2.1+"px";
second.style.marginLeft = height/4+"px";
second.style.marginTop = height/4+"px";
var second = document.getElementsByClassName('second')[0];`
var left = (screen.width/2)-(100/2);
var top = (screen.height/2)-(100/2);
second.style.width = "100px"; //set as per your requirement
second.style.height = "100px"; //set as per your requirement
second.style.left= left +"px";
second.style.top = top +"px";
Just in case, you're interested, I tried to come up with a CSS only solution.
http://jsfiddle.net/53M6A/1/
Here's the changes I made to the .second class.
.second{
left: 50%; //move 50% to left
top: 50%; // move 50% down
margin-left: -50px; //move half of it's own size back to the left
margin-top: -50px; //move half of it's own size back to the top
position: relative; //make it relative, so it can be moved around by left/top
width:100px;
height:100px;
background: #fff;
border-radius:50%;
}
I've been playing a little in your fiddle and finally, I changed your 2 last lines for these:
first.style.display = "table-cell";
first.style.verticalAlign = "middle";
second.style.margin = "0 auto";
Fiddle
Seems perfectly centered to me.

Categories