Normally I don't post myself I usually find what I need via other's threads so I'm sorry if any of this is in the wrong place or improperly formatted. I've never done this before really.
So here's the situation:
I'm trying to rebuild my website and I opted to go with the X Theme for WordPress. Mostly it's going great, but the few times I've wanted to customize and go around X it's proved a bit more difficult. If you know of a way to do this within X that would accomplish this without doing custom coding, I'm all ears.
So here's what I'm trying to do:
I had an idea for a circular menu that would position it's elements to where the top one is the "selected" element of the menu. So it would look something like this in terms of layout:
(Sorry, apparently I'm too new to use images in my posts :/)
Basic State: http://i.stack.imgur.com/Gs2Nz.jpg
Now when a user were to click on an item, I'd like it to rotate the new selected item up to the top where the "1" item was in the previous image. So it would be like this:
Menu Items Are Rotated If The User Selected Item 3: http://i.stack.imgur.com/KWseu.jpg
Some other things to note:
I want the text or images of the menu items to always be normally aligned, in other words I don't want the elements text to be upside down or something after it rotates.
The original positioning of the elements I'd like to be handle when the page loads instead of hardcoded in the CSS style. Mainly just so that it can be done dynamically.
I'm planning to do more with the menu but it's THIS behavior that I'm having problems with.
I've tried things like Jquery's Animate() method, or using JavaScript to affect each elements css "top" & "left" properties, but it just doesn't seem to be working as the elements don't seem to want to move.
I don't know that this isn't a problem with trying to go through X's customizer area or not as that's where I was told to add JavaScript code. Or this could have to do with me not connecting the JavaScript/JQuery code with the CSS properly, I have a decent amount of coding experience, but I'm relatively new to JQuery/CSS etc.
So short version:
I'm trying to find a way that when the page loads the elements are positioned dynamically around a center point. Then when a user clicks on an element all the elements rotate around the center, till the newly selected item is at the top. This behavior should continue as the user selects different items.
Sorry for this being a long post, but I'm just trying to explain as best I can. Any insight or advice would be greatly appreciated! Thanks in advance! :)
UPDATE:
So I ended up trying marzelin's answer as it looked perfect for what I wanted. However, when I added it into the X-Theme's Javascript area and updated my CSS the elements aren't moving. They all stack in the center, but they don't encircle the center point and clicking on them doesn't seem to be doing anything. Seems like the CSS has taken affect but the Javascript part is not affecting the elements for some reason?
Here's the marzelin's answer I used (just the JavaScript part):
const buttons = Array.from(document.querySelectorAll('.button'))
const count = buttons.length
const increase = Math.PI * 2 / buttons.length
const radius = 150
let angle = 0
buttons.forEach((button, i) => {
button.style.top = Math.sin(-Math.PI / 2 + i * increase) * radius + 'px'
button.style.left = Math.cos(-Math.PI / 2 + i * increase) * radius + 'px'
button.addEventListener('click', move)
})
function move(e) {
const n = buttons.indexOf(e.target)
const endAngle = (n % count) * increase
turn()
function turn() {
if (Math.abs(endAngle - angle) > 1/8) {
const sign = endAngle > angle ? 1 : -1
angle = angle + sign/8
setTimeout(turn, 20)
} else {
angle = endAngle
}
buttons.forEach((button, i) => {
button.style.top = Math.sin(-Math.PI / 2 + i * increase - angle) * radius + 'px'
button.style.left = Math.cos(-Math.PI / 2 + i * increase - angle) * radius + 'px'
})
}
}
Here's how my the javascript section of my X-Theme looks at the moment (excluding other code for other functions, like hiding my navbar and such):
jQuery(function($){
/* javascript or jquery code goes here */
const stars = Array.from(document.querySelectorAll('.btnStars'));
const count = stars.length;
const increase = Math.PI * 2 / stars.length;
const radius = 300;
let angle = 0;
stars.forEach((star, i) => {
star.style.top = Math.sin(-Math.PI / 2 + i * increase) * radius + 'px';
star.style.left = Math.cos(-Math.PI / 2 + i * increase) * radius + 'px';
});
$('.btnStar').click(function(e) {
const n = stars.indexOf(e.target);
const endAngle = (n % count) * increase;
function turn() {
if (Math.abs(endAngle - angle) > 1/8) {
const sign = endAngle > angle ? 1 : -1;
angle = angle + sign/8;
setTimeout(turn, 20);
} else {
angle = endAngle;
}
stars.forEach((star, i) => {
star.style.top = Math.sin(-Math.PI / 2 + i * increase - angle) * radius + 'px';
star.style.left = Math.cos(-Math.PI / 2 + i * increase - angle) * radius + 'px';
})
}
turn();
});
});
I did change a few things, namely the CSS class names and such, but most of it's the same. I did a couple things like reorganizing as the editor for X Theme didn't seem to know what a few of the functions were so I moved them to before their calls and then it seemed to find them. So little things like that.
I also tried to change the move function to a JQuery .click function to see if that would trigger anything but it didn't seem to change anything.
While I've worked with Javascript and some JQuery before, I've never really dealt with trying to incorporate it into a WordPress theme so I really don't know what this isn't working.
Does anyone see anything I'm doing wrong? Cause I'm pretty perplexed as to why this won't work. :/
Simple MVP
const buttons = Array.from(document.querySelectorAll('.button'))
const count = buttons.length
const increase = Math.PI * 2 / buttons.length
const radius = 150
buttons.forEach((button, i) => {
button.style.top = Math.sin(-Math.PI / 2 + i * increase) * radius + 'px'
button.style.left = Math.cos(-Math.PI / 2 + i * increase) * radius + 'px'
button.addEventListener('click', move)
})
function move(e) {
const n = buttons.indexOf(e.target)
buttons.forEach((button, i) => {
button.style.top = Math.sin(-Math.PI / 2 + (i - n % count) * increase) * radius + 'px'
button.style.left = Math.cos(-Math.PI / 2 + (i - n % count) * increase) * radius + 'px'
})
}
html,
body {
height: 100%;
}
.menu {
height: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
background-color: seagreen;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
}
.center {
width: 100px;
height: 100px;
background-color: goldenrod;
border-radius: 100%;
position: relative;
line-height: 100px;
text-align: center;
}
.button {
position: absolute;
width: 100px;
height: 100px;
border-radius: 100%;
-webkit-transition: all 0.5s;
transition: all 0.5s;
background-color: pink;
line-height: 100px;
text-align: center;
}
<div class="menu">
<div class="center">Menu
<div class="button">1</div>
<div class="button">2</div>
<div class="button">3</div>
<div class="button">4</div>
<div class="button">5</div>
</div>
</div>
Circular Motion
const buttons = Array.from(document.querySelectorAll('.button'))
const count = buttons.length
const increase = Math.PI * 2 / buttons.length
const radius = 150
let angle = 0
buttons.forEach((button, i) => {
button.style.top = Math.sin(-Math.PI / 2 + i * increase) * radius + 'px'
button.style.left = Math.cos(-Math.PI / 2 + i * increase) * radius + 'px'
button.addEventListener('click', move)
})
function move(e) {
const n = buttons.indexOf(e.target)
const endAngle = (n % count) * increase
turn()
function turn() {
if (Math.abs(endAngle - angle) > 1/8) {
const sign = endAngle > angle ? 1 : -1
angle = angle + sign/8
setTimeout(turn, 20)
} else {
angle = endAngle
}
buttons.forEach((button, i) => {
button.style.top = Math.sin(-Math.PI / 2 + i * increase - angle) * radius + 'px'
button.style.left = Math.cos(-Math.PI / 2 + i * increase - angle) * radius + 'px'
})
}
}
html, body {
height: 100%;
}
.menu {
height: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
background-color: seagreen;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
line-height: 100px;
text-align: center;
}
.center {
width: 100px;
height: 100px;
background-color: goldenrod;
border-radius: 100%;
position: relative;
}
.button {
position: absolute;
width: 100px;
height: 100px;
border-radius: 100%;
background-color: pink;
line-height: 100px;
text-align: center;
cursor: pointer;
}
<div class="menu">
<div class="center">menu
<div class="button">1</div>
<div class="button">2</div>
<div class="button">3</div>
<div class="button">4</div>
<div class="button">5</div>
</div>
</div>
The trigonometrical approach here feels wrong.
It's pretty much like trying to program in binary code. It's doable, but not necessarily how our programs should look like, if we want to keep our ability to read the code later on and maybe further modify its logic.
In order not to have to calculate the position of each menu element, we'd have to separate the rotation of the menu from the rotation of each axis.
Once those are separated, their values could be placed in CSS variables, rotating both the element they're aimed at (menu or axis) while rotating the respective button backwards by the same amount. This way, the buttons will always stand upright, because the rotations cancel each other out.
Here's a demo of the principle. Notice the use of CSS variables,
using style="{ '--var-name': value }". You can also inspect the markup during runtime to read the current rotation values:
new Vue({
el: '#app',
data: () => ({
buttons: 3,
useTransitions: true,
isMenuOpen: true,
rotation: -90
}),
computed: {
axisRotations() {
return Array.from({
length: this.buttons
}).map((_, i) => 360 * (this.buttons - i) / this.buttons)
},
menuRotation: {
get() {
return this.rotation
},
set(val) {
this.rotation = isNaN(Number(val)) ? -90 : Number(val)
}
}
},
methods: {
updateButtons(n) {
if (this.buttons + n > 0) {
this.buttons += n;
this.isMenuOpen = true;
this.menuRotation = -90;
}
},
goToTop(axis) {
let diff = this.degreesToTop(axis);
diff = diff > 180
? diff - 360
: diff <= -180
? diff + 360
: diff;
this.menuRotation = Math.round((this.menuRotation + diff) * 10) / 10;
},
degreesToTop(axis) {
return (Math.round(this.axisRotations[axis - 1]) - this.menuRotation - 90) % 360;
},
isActive(axis) {
return !(this.degreesToTop(axis));
},
toggleMenu() {
this.isMenuOpen = !this.isMenuOpen;
}
}
})
.menu {
width: 0;
height: 0;
top: 110px;
left: 110px;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
transform: rotate(var(--menu-rotation));
--menu-rotation: 0deg;
}
.menu .center {
height: 54px;
min-width: 54px;
border-radius: 27px;
display: flex;
align-items: center;
justify-content: center;
background-color: white;
border: 1px solid #eee;
cursor: pointer;
z-index: 2;
transform: rotate(calc(-1 * var(--menu-rotation))) translateZ(0);
}
.menu .axis {
position: absolute;
width: 100px;
left: 0;
height: 0;
display: flex;
align-items: center;
justify-content: flex-end;
transform-origin: 0 0;
transform: rotate(var(--axis-rotation));
}
.animated .axis.axis {
transition: all .54s cubic-bezier(.4, 0, .2, 1);
}
.menu .axis.closed {
width: 27px;
transform: rotate(calc(var(--axis-rotation) + 180deg));
opacity: .1;
}
.axis.closed button,
.axis.active button {
color: white;
background-color: #f50;
}
.axis.active:not(.closed) {
z-index: 1;
}
.axis button {
background-color: white;
cursor: pointer;
width: 54px;
height: 54px;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
border-radius: 27px;
border: 1px solid #eee;
transform: rotate(calc(calc(-1 * var(--axis-rotation)) - var(--menu-rotation))) translateZ(0);
outline: none;
}
.flexer {
display: flex;
height: 240px;
padding-left: 220px;
}
.controls {
flex-grow: 1
}
input {
width: 100%;
}
label input {
width: auto;
}
label {
display: block;
margin-top: 1rem;
cursor: pointer;
}
.animated,
.animated .center,
.animated .axis,
.animated .axis>* {
transition: transform .35s cubic-bezier(.4, 0, .2, 1);
}
body {
background-color: #f8f8f8;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12"></script>
<div id="app">
<div>
<div class="flexer">
<div class="menu"
:class="{ animated: useTransitions }"
:style="{'--menu-rotation': `${menuRotation}deg`}">
<div class="center" #click="toggleMenu">menu</div>
<div v-for="axis in buttons"
class="axis"
:class="{ closed: !isMenuOpen, active: isActive(axis) }"
:style="{'--axis-rotation': `${360 * (axis - 1) / buttons}deg`}">
<button v-text="axis" #click="goToTop(axis)" />
</div>
</div>
<div class="controls">
Menu rotation (<code v-text="`${menuRotation}deg`"></code>)
<input type="range" min="-720" max="720" v-model="menuRotation">
<button #click="updateButtons(1)">Add button</button>
<button #click="updateButtons(-1)">Remove button</button>
<button #click="toggleMenu">Toggle menu</button>
<label>
<input type="checkbox" v-model="useTransitions">Use transitions
</label>
</div>
</div>
<pre v-text="{ menuRotation, buttons, axisRotations }"></pre>
</div>
</div>
As you can see, I'm never calculating positions of buttons. The only trigonometry used is "there are 360 degrees in a circle".
The above example is done in Vue, as it's a fast prototyping tool I happen to like. If you want a vanilla solution of getting the items to top, see my answer on a follow-up question to this one.
Related
I used pixi js on my website to create a dynamic gradient animation with shape-changing blobs. The animation works perfectly fine the only problem I am facing is when I run page speed tests test it assumes the page has not been rendered since the animation is running still and gives it astronomical loading times.
I assume the part of the problem is page speed runs until the javascript has finshed executing which never will since this is a 2d animation.
If anyone has any idea on how to solve this problem please let me know.
import * as PIXI from "https://cdn.skypack.dev/pixi.js";
import { KawaseBlurFilter } from "https://cdn.skypack.dev/#pixi/filter-kawase-blur";
import SimplexNoise from "https://cdn.skypack.dev/simplex-noise";
import hsl from "https://cdn.skypack.dev/hsl-to-hex";
import debounce from "https://cdn.skypack.dev/debounce";
// return a random number within a range
function random(min, max) {
return Math.random() * (max - min) + min;
}
// map a number from 1 range to another
function map(n, start1, end1, start2, end2) {
return ((n - start1) / (end1 - start1)) * (end2 - start2) + start2;
}
// Create a new simplex noise instance
const simplex = new SimplexNoise();
// ColorPalette class
class ColorPalette {
constructor() {
this.setColors();
this.setCustomProperties();
}
setColors() {
// pick a random hue somewhere between 220 and 360
this.hue = ~~random(220, 360);
this.complimentaryHue1 = this.hue + 30;
this.complimentaryHue2 = this.hue + 60;
// define a fixed saturation and lightness
this.saturation = 95;
this.lightness = 50;
// define a base color
this.baseColor = hsl(this.hue, this.saturation, this.lightness);
// define a complimentary color, 30 degress away from the base
this.complimentaryColor1 = hsl(
this.complimentaryHue1,
this.saturation,
this.lightness
);
// define a second complimentary color, 60 degrees away from the base
this.complimentaryColor2 = hsl(
this.complimentaryHue2,
this.saturation,
this.lightness
);
// store the color choices in an array so that a random one can be picked later
this.colorChoices = [
this.baseColor,
this.complimentaryColor1,
this.complimentaryColor2
];
}
randomColor() {
// pick a random color
return this.colorChoices[~~random(0, this.colorChoices.length)].replace(
"#",
"0x"
);
}
setCustomProperties() {
// set CSS custom properties so that the colors defined here can be used throughout the UI
document.documentElement.style.setProperty("--hue", this.hue);
document.documentElement.style.setProperty(
"--hue-complimentary1",
this.complimentaryHue1
);
document.documentElement.style.setProperty(
"--hue-complimentary2",
this.complimentaryHue2
);
}
}
// Orb class
class Orb {
// Pixi takes hex colors as hexidecimal literals (0x rather than a string with '#')
constructor(fill = 0x000000) {
// bounds = the area an orb is "allowed" to move within
this.bounds = this.setBounds();
// initialise the orb's { x, y } values to a random point within it's bounds
this.x = random(this.bounds["x"].min, this.bounds["x"].max);
this.y = random(this.bounds["y"].min, this.bounds["y"].max);
// how large the orb is vs it's original radius (this will modulate over time)
this.scale = 1;
// what color is the orb?
this.fill = fill;
// the original radius of the orb, set relative to window height
this.radius = random(window.innerHeight / 6, window.innerHeight / 3);
// starting points in "time" for the noise/self similar random values
this.xOff = random(0, 1000);
this.yOff = random(0, 1000);
// how quickly the noise/self similar random values step through time
this.inc = 0.002;
// PIXI.Graphics is used to draw 2d primitives (in this case a circle) to the canvas
this.graphics = new PIXI.Graphics();
this.graphics.alpha = 0.825;
// 250ms after the last window resize event, recalculate orb positions.
window.addEventListener(
"resize",
debounce(() => {
this.bounds = this.setBounds();
}, 250)
);
}
setBounds() {
// how far from the { x, y } origin can each orb move
const maxDist =
window.innerWidth < 1000 ? window.innerWidth / 3 : window.innerWidth / 5;
// the { x, y } origin for each orb (the bottom right of the screen)
const originX = window.innerWidth / 1.25;
const originY =
window.innerWidth < 1000
? window.innerHeight
: window.innerHeight / 1.375;
// allow each orb to move x distance away from it's x / y origin
return {
x: {
min: originX - maxDist,
max: originX + maxDist
},
y: {
min: originY - maxDist,
max: originY + maxDist
}
};
}
update() {
// self similar "psuedo-random" or noise values at a given point in "time"
const xNoise = simplex.noise2D(this.xOff, this.xOff);
const yNoise = simplex.noise2D(this.yOff, this.yOff);
const scaleNoise = simplex.noise2D(this.xOff, this.yOff);
// map the xNoise/yNoise values (between -1 and 1) to a point within the orb's bounds
this.x = map(xNoise, -1, 1, this.bounds["x"].min, this.bounds["x"].max);
this.y = map(yNoise, -1, 1, this.bounds["y"].min, this.bounds["y"].max);
// map scaleNoise (between -1 and 1) to a scale value somewhere between half of the orb's original size, and 100% of it's original size
this.scale = map(scaleNoise, -1, 1, 0.5, 1);
// step through "time"
this.xOff += this.inc;
this.yOff += this.inc;
}
render() {
// update the PIXI.Graphics position and scale values
this.graphics.x = this.x;
this.graphics.y = this.y;
this.graphics.scale.set(this.scale);
// clear anything currently drawn to graphics
this.graphics.clear();
// tell graphics to fill any shapes drawn after this with the orb's fill color
this.graphics.beginFill(this.fill);
// draw a circle at { 0, 0 } with it's size set by this.radius
this.graphics.drawCircle(0, 0, this.radius);
// let graphics know we won't be filling in any more shapes
this.graphics.endFill();
}
}
// Create PixiJS app
const app = new PIXI.Application({
// render to <canvas class="orb-canvas"></canvas>
view: document.querySelector(".orb-canvas"),
// auto adjust size to fit the current window
resizeTo: window,
// transparent background, we will be creating a gradient background later using CSS
transparent: true
});
// Create colour palette
const colorPalette = new ColorPalette();
app.stage.filters = [new KawaseBlurFilter(30, 10, true)];
// Create orbs
const orbs = [];
for (let i = 0; i < 10; i++) {
const orb = new Orb(colorPalette.randomColor());
app.stage.addChild(orb.graphics);
orbs.push(orb);
}
// Animate!
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
app.ticker.add(() => {
orbs.forEach((orb) => {
orb.update();
orb.render();
});
});
} else {
orbs.forEach((orb) => {
orb.update();
orb.render();
});
}
document
.querySelector(".overlay__btn--colors")
.addEventListener("click", () => {
colorPalette.setColors();
colorPalette.setCustomProperties();
orbs.forEach((orb) => {
orb.fill = colorPalette.randomColor();
});
});
:root {
--dark-color: hsl(var(--hue), 100%, 9%);
--light-color: hsl(var(--hue), 95%, 98%);
--base: hsl(var(--hue), 95%, 50%);
--complimentary1: hsl(var(--hue-complimentary1), 95%, 50%);
--complimentary2: hsl(var(--hue-complimentary2), 95%, 50%);
--font-family: "Poppins", system-ui;
--bg-gradient: linear-gradient(
to bottom,
hsl(var(--hue), 95%, 99%),
hsl(var(--hue), 95%, 84%)
);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
max-width: 1920px;
min-height: 100vh;
display: grid;
place-items: center;
padding: 2rem;
font-family: var(--font-family);
color: var(--dark-color);
background: var(--bg-gradient);
}
.orb-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
}
strong {
font-weight: 600;
}
.overlay {
width: 100%;
max-width: 1140px;
max-height: 640px;
padding: 8rem 6rem;
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.375);
box-shadow: 0 0.75rem 2rem 0 rgba(0, 0, 0, 0.1);
border-radius: 2rem;
border: 1px solid rgba(255, 255, 255, 0.125);
}
.overlay__inner {
max-width: 36rem;
}
.overlay__title {
font-size: 1.875rem;
line-height: 2.75rem;
font-weight: 700;
letter-spacing: -0.025em;
margin-bottom: 2rem;
}
.text-gradient {
background-image: linear-gradient(
45deg,
var(--base) 25%,
var(--complimentary2)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
-moz-background-clip: text;
-moz-text-fill-color: transparent;
}
.overlay__description {
font-size: 1rem;
line-height: 1.75rem;
margin-bottom: 3rem;
}
.overlay__btns {
width: 100%;
max-width: 30rem;
display: flex;
}
.overlay__btn {
width: 50%;
height: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.875rem;
font-weight: 600;
color: var(--light-color);
background: var(--dark-color);
border: none;
border-radius: 0.5rem;
transition: transform 150ms ease;
outline-color: hsl(var(--hue), 95%, 50%);
}
.overlay__btn:hover {
transform: scale(1.05);
cursor: pointer;
}
.overlay__btn--transparent {
background: transparent;
color: var(--dark-color);
border: 2px solid var(--dark-color);
border-width: 2px;
margin-right: 0.75rem;
}
.overlay__btn-emoji {
margin-left: 0.375rem;
}
a {
text-decoration: none;
color: var(--dark-color);
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* Not too many browser support this yet but it's good to add! */
#media (prefers-contrast: high) {
.orb-canvas {
display: none;
}
}
#media only screen and (max-width: 1140px) {
.overlay {
padding: 8rem 4rem;
}
}
#media only screen and (max-width: 840px) {
body {
padding: 1.5rem;
}
.overlay {
padding: 4rem;
height: auto;
}
.overlay__title {
font-size: 1.25rem;
line-height: 2rem;
margin-bottom: 1.5rem;
}
.overlay__description {
font-size: 0.875rem;
line-height: 1.5rem;
margin-bottom: 2.5rem;
}
}
#media only screen and (max-width: 600px) {
.overlay {
padding: 1.5rem;
}
.overlay__btns {
flex-wrap: wrap;
}
.overlay__btn {
width: 100%;
font-size: 0.75rem;
margin-right: 0;
}
.overlay__btn:first-child {
margin-bottom: 1rem;
}
}
<!-- Canvas -->
<canvas class="orb-canvas"></canvas>
<!-- Overlay -->
<div class="overlay">
<!-- Overlay inner wrapper -->
<div class="overlay__inner">
<!-- Title -->
<h1 class="overlay__title">
Hey, would you like to learn how to create a
<span class="text-gradient">generative</span> UI just like this?
</h1>
<!-- Description -->
<p class="overlay__description">
In this tutorial we will be creating a generative “orb” animation
using pixi.js, picking some lovely random colors and pulling it all
together in a nice frosty UI.
<strong>We're gonna talk accessibility, too.</strong>
</p>
<!-- Buttons -->
<div class="overlay__btns">
<button class="overlay__btn overlay__btn--transparent">
View
</a>
</button>
<button class="overlay__btn overlay__btn--colors">
<span>Randomise Colors</span>
<span class="overlay__btn-emoji">🎨</span>
</button>
</div>
</div>
</div>
Try execute your "pixi related code" after page is loaded. So i mean this code:
// Create a new simplex noise instance
const simplex = new SimplexNoise();
...
// Create PixiJS app
const app = new PIXI.Application({
// render to <canvas class="orb-canvas"></canvas>
view: document.querySelector(".orb-canvas"),
// auto adjust size to fit the current window
resizeTo: window,
// transparent background, we will be creating a gradient background later using CSS
transparent: true
});
// Create colour palette
const colorPalette = new ColorPalette();
app.stage.filters = [new KawaseBlurFilter(30, 10, true)];
// Create orbs
const orbs = [];
for (let i = 0; i < 10; i++) {
const orb = new Orb(colorPalette.randomColor());
app.stage.addChild(orb.graphics);
orbs.push(orb);
}
// Animate!
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
app.ticker.add(() => {
orbs.forEach((orb) => {
orb.update();
orb.render();
});
});
} else {
orbs.forEach((orb) => {
orb.update();
orb.render();
});
}
document
.querySelector(".overlay__btn--colors")
.addEventListener("click", () => {
colorPalette.setColors();
colorPalette.setCustomProperties();
orbs.forEach((orb) => {
orb.fill = colorPalette.randomColor();
});
});
Execute all this not "as soon as possible" (as you do now - which causes page speed to count it as page load time) - but after page is loaded. Additionally you can also add "setTimeout" there with 1 second delay - so it will be: "after page load plus 1 second".
See some proposed solutions like these: Run a certain script JS after x seconds that the page is loaded
The code
function OpenMenu(){
var MenuRotate = getCurrentRotation(document.getElementById("Menu-Icon"));
document.getElementById("menu").style.margin = "450px";
document.getElementById("Menu-Icon").style.transform ="rotate(90deg)";
var Menu1 = 1;
//alert(MenuRotate);
function getCurrentRotation(el){
var st = window.getComputedStyle(el, null);
var tm = st.getPropertyValue("-webkit-transform") ||
st.getPropertyValue("-moz-transform") ||
st.getPropertyValue("-ms-transform") ||
st.getPropertyValue("-o-transform") ||
st.getPropertyValue("transform") ||
"none";
if (tm != "none") {
var values = tm.split('(')[1].split(')')[0].split(',');
/*
a = values[0];
b = values[1];
angle = Math.round(Math.atan2(b,a) * (180/Math.PI));
*/
//return Math.round(Math.atan2(values[1],values[0]) * (180/Math.PI)); //this would return negative values the OP doesn't wants so it got commented and the next lines of code added
var angle = Math.round(Math.atan2(values[1],values[0]) * (180/Math.PI));
return (angle < 0 ? angle + 360 : angle); //adding 360 degrees here when angle < 0 is equivalent to adding (2 * Math.PI) radians before
}
return 0;
}
I would like to be able to add MenuRotate to the 90 degrees, not sure how i could do this so im looking for some answers
I read the article that the OP code is originally from and I believe it's overkill. What should be done to avoid so much work is to setup the elements angles initially so you know what to start from or reset the elements to 0.
Example A features a <form> that allows the user to rotate an element by adding positive and/or negative numbers (min -360, max 360).
Example B features a function that operates the same as the event handler (spin(e)) in Example A.
Details are commented in both examples
Example A
<form> as User Interface
// Bind <form> to the submit event
document.forms.spin.onsubmit = spin;
function spin(e) {
// Stop normal behavior when submit is triggered
e.preventDefault();
// Reference all form controls
const IO = this.elements;
// Reference <output>
const comp = IO.compass;
// Reference <input>
const turn = IO.turn;
// Get <input> value and convert it into a number
let deg = +turn.value;
// Add comp value with turn value and assign to comp value
comp.value = +comp.value +(deg);
// If comp value is ever over 360, reset it
if (+comp.value > 360) {
comp.value = +comp.value - 360;
}
// .cssText is like .textContent for the style property
comp.style.cssText = `transform: rotate(${comp.value}deg)`;
}
fieldset {
display: flex;
justify-content: center;
align-items: center;
}
#turn {
width: 3rem;
text-align: center;
}
#compass {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 100px;
border-radius: 50%;
background: rgba(0, 0, 255, 0.3);
}
#compass::before {
content: '➤';
position: absolute;
z-index: 1;
transform: rotate(-90deg) translate(55%, -5%);
transform-origin: center center;
font-size: 3rem;
}
<form id='spin'>
<fieldset>
<input id='turn' type='number' min='-360' max='360' step='any'><input id='add' type='submit' value='Add'>
</fieldset>
<fieldset>
<output id='compass' value='0'></output>
</fieldset>
</form>
Example B
No <form>, Only a Function
// Declare variable to track angle
let degree;
/**
* #desc - Rotates a given element by a given number of
* degrees.
* #param {object<DOM>} node - The element to rotate
* #param {number} deg - The number of degrees to rotate
* #param {boolean} init - If true the element's rotate value
* will be 0 and degree = 0 #default is false
*/
function turn(node, deg, init = false) {
// If true reset node rotate and degree to 0
if (init) {
node.style.cssText = `transform: rotate(0deg)`;
degree = 0;
}
/*
Simple arithmatic
Reset degrees when more than 360
*/
degree = degree + deg;
if (degree > 360) {
degree = degree - 360;
}
// .cssText is like .textContent for the style property
node.style.cssText = `transform: rotate(${degree}deg)`;
console.log(node.id + ': ' + degree);
}
const c = document.getElementById('compass');
turn(c, 320, true);
fieldset {
display: flex;
justify-content: center;
align-items: center;
}
#compass {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 100px;
border-radius: 50%;
background: rgba(0, 0, 255, 0.3);
}
#compass::before {
content: '➤';
position: absolute;
z-index: 1;
transform: rotate(-90deg) translate(55%, -5%);
transform-origin: center center;
font-size: 3rem;
}
<fieldset>
<output id='compass' value='0'></output>
</fieldset>
I'm trying to make an infinite marquee (scrolling horizontal text), but on scroll it speeds up the translation of the HTML elements. I am achieving this effect by using a linear interpolation function.
You can see the effect on this site that I'm trying to remake: https://altsdigital.com/ It says "Not your usual SEO agency"
Mine almost works - the problem is that when my HTML resets its position - my text overlaps and briefly translates to the left before correcting. Keep your eyes on the left side of the page. You will see that the text overlaps at one brief moment then translates left (during it's movement to the right), it eventually corrects itself as it plays retaining the original gap. You can see in this screenshot the "t" and "I" are overlapping. Shortly after this, the text on the left translates left and there is a gap between the letters. I want it to have a gap and not briefly translate left.
I have no idea how to fix this - I've tried calling the lerp function on scroll but nothing seems to change. Thanks in advance.
Here's the code:
const lerp = (current, target, factor) => {
let holder = current * (1 - factor) + target * factor;
holder = parseFloat(holder).toFixed(3);
return holder;
};
class LoopingText {
constructor(DOMElements) {
this.DOMElements = DOMElements;
this.lerpingData = {
counterOne: { current: 0, target: 0 },
counterTwo: { current: 100, target: 100 },
};
this.interpolationFactor = 0.1;
this.direction = true;
this.speed = 0.2;
this.render();
this.onScroll();
}
onScroll() {
window.addEventListener("scroll", () => {
this.lerpingData["counterOne"].target += this.speed * 5;
this.lerpingData["counterTwo"].target += this.speed * 5;
});
}
lerp() {
for (const counter in this.lerpingData) {
this.lerpingData[counter].current = lerp(
this.lerpingData[counter].current,
this.lerpingData[counter].target,
this.interpolationFactor
);
}
this.lerpingData["counterOne"].target += this.speed;
this.lerpingData["counterTwo"].target += this.speed;
if (this.lerpingData["counterOne"].target < 100) {
this.DOMElements[0].style.transform = `translate(${this.lerpingData["counterOne"].current}%, 0%)`;
} else {
this.lerpingData["counterOne"].current = -100;
this.lerpingData["counterOne"].target = -100;
}
if (this.lerpingData["counterTwo"].target < 100) {
this.DOMElements[1].style.transform = `translate(${this.lerpingData["counterTwo"].current}%, 0%)`;
} else {
this.lerpingData["counterTwo"].current = -100;
this.lerpingData["counterTwo"].target = -100;
}
}
render() {
this.lerp();
window.requestAnimationFrame(() => this.render());
}
}
let textArray = document.getElementsByClassName("item");
new LoopingText(textArray);
#import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght#0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Poppins";
}
.hero-section {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
overflow: hidden;
position: relative;
width: 100%;
}
.loop-container {
position: relative;
width: 100%;
display: flex;
/* padding-right: 24px; */
}
.item {
position: absolute;
font-size: 15rem;
white-space: nowrap;
margin: 0;
}
span {
transition: all 0.2s;
cursor: default;
}
.hover:hover {
color: gray;
transition: all 0.2s;
}
<body>
<section class="hero-section">
<div class="loop-container">
<div class="item">Infinite Horizontal Looping Text</div>
<div class="item">Infinite Horizontal Looping Text</div>
</div>
</section>
<section class="hero-section">
</section>
</body>
I have a red container which initially is at bottom right of black container. I have a scale function that gradually scales the container. I want to make the bottom right position of red container to be fixed and scale it towards top left. How can I do that?
const box = document.getElementById("box")
const initHeight = 200
const initWidth = 200
const centerX = initWidth / 2
const centerY = initHeight / 2
function transform(scale, translate) {
if (translate) {
translate[0] = -centerX + translate[0]
translate[1] = -centerY + translate[1]
}
box.style.transform = `scale(${scale})${
translate ? ` translate(${translate.map((x) => x + "px").toString()})` : ""
}`
}
let initX = initWidth
let initY = initHeight
let scaleVal = 0.5
transform(scaleVal, [initX, initY])
function scale() {
scaleVal = scaleVal + 0.01
transform(scaleVal, [
initX - scaleVal * initWidth,
initY - scaleVal * initHeight
])
if (scaleVal <= 1) {
setTimeout(() => {
requestAnimationFrame(scale)
}, 50)
}
}
scale()
* {
box-sizing: border-box;
}
.box {
height: 200px;
width: 200px;
background-color: black;
position: absolute;
}
.box:nth-child(2) {
background-color: red;
}
<div id="app">
<div class="box"></div>
<div class="box" id="box"></div>
</div>
To lock the bottom right corner of the red box to the bottom right of the black box this snippet does two things: positions red box right bottom relative to the parent app container and sets the transform-origin to that spot too (normally transform origin is at the center of an element). It then uses a CSS animation to expand the red box and contract it again using scale.
This method does not need JS as it is a simple scaling transform, but of course some of the subtleties of the original transformations are lost because of tying the corner down.
* {
box-sizing: border-box;
}
body {
position: relative;
width: 100vw;
height: 100vh;
}
#app {
position: absolute;
height: 200px;
width: 200px;
}
.box:nth-child(1) {
height: 200px;
width: 200px;
background-color: black;
position: absolute;
top: 0;
left: 0;
}
#box {
background-color: red;
width: 100px;
height: 100px;
position: absolute;
bottom: 0;
right: 0;
transform-origin: right bottom;
animation: scale 5s 1 ease-in-out;
}
#keyframes scale {
0% {
transform: scale(1);
}
50% {
transform: scale(2);
}
100% {
transform: scale(1);
}
}
<div id="app">
<div class="box"></div>
<div class="box" id="box"></div>
</div>
Okay so I finally figured it out,
const box = document.getElementById("box")
let scale = 0
const initWidth = 50
const initHeight = 50
function fixed(num, fix = 1) {
return Number(parseFloat(num).toFixed(fix))
}
function scaleBox() {
const [x, y] = [
fixed((initWidth - scale * initWidth) / 2),
fixed((initHeight - scale * initHeight) / 2)
]
box.style.transform = `translate(${x}px, ${y}px) scale(${scale})`
scale = scale + 0.1
if (scale < 1) {
setTimeout(() => {
requestAnimationFrame(scaleBox)
}, 500)
}
}
scaleBox()
* {
box-sizing: border-box;
}
.box {
height: 50px;
width: 50px;
background-color: black;
position: absolute;
}
.box:nth-child(2) {
background-color: red;
transform: translate(0, 0) scale(0);
}
<div id="app">
<div class="box"></div>
<div class="box" id="box"></div>
</div>
Explanation
The trick is to translate the container in such a way that when its scaled after the translation, it always places itself in the bottom right of purple container.
To figure out the translation amount, let's first scale the container to 0.5 without any translation. It looks like this,
As you can see the container's width is 25 as 0.5(scale) * 50(init_width)=25 and position from container from all sides(top left, bottom left, top right, bottom right) will be (25/2, 25/2)=(12.5,12.5) since the container is scaled equally from all sides.
Since the position from bottom right is (12.5,12.5), we need to translate the container to (+12.5,+12.5) and then scale it to exactly place it at bottom right.
You can achieve many things using display:flex, it's great!
This is how I would approach your problem:
const handleClick = () => {
const blackDiv = document.getElementById("black-div");
const redDiv = document.getElementById("red-div");
let widthRatio = 0;
let heightRatio = 0;
const scaleUpTimer = setInterval(() => {
if (widthRatio === 1 || heightRatio === 1) clearInterval(scaleUpTimer);
widthRatio = redDiv.offsetWidth / blackDiv.offsetWidth;
heightRatio = redDiv.offsetHeight / blackDiv.offsetHeight;
redDiv.style.width = widthRatio * 100 + 2 + "%";
redDiv.style.height = heightRatio * 100 + 2 + "%";
}, 10);
};
#black-div {
width: 200px;
height: 200px;
background-color: rgb(0, 0, 0);
display: flex;
align-items: flex-end;
justify-content: flex-end;
}
#red-div {
background-color: red;
width: 50%;
height: 50%;
}
<div id='black-div'>
<div id='red-div' onclick={handleClick()}></div>
</div>
EDIT: I used onclick here but obviously you would have to handle the situations where someone clicks the red square and its already scaled up to avoid setting unnecessary timers. Or you could just call the function directly, without having to click anything.
I have created a very basic animated percentage bar with HTML, CSS, and JS the only problem is that I am trying to devise a way to also animate the increase and/or decrease of the percentage output to go along with an animated percentage bar. In the example below and in this JsFiddle I have successfully created that with the only problem being that it doesn't seem to be the most efficient or effective way of doing it.
In the code snippet directly below I'm creating this animated effect by...
Setting x equal to setInterval
Capturing the width of percent bar on the left and removing the px from the end of the string.
Capturing the width of percent bar on the right and removing the px from the end of the string.
Displays the percent value for the left (blue) bar inside the tooltip that can be seen when hovered over.
Displays the percent value for the right (red) bar inside the tooltip that can be seen when hovered over.
Displays the percent value of the left (blue) bar below the percent bar.
Displays the percent value of the right (red) bar below the percent bar.
All of this code below will run every 64 Milliseconds.
This code will only run for 2000 Milliseconds which is the same amount of time that I have set the transition for the percent bars.
Note: The whole point of the code below is to give the illusion that the percent values are increasing as either of the percent bars are increasing. In short, the goal is to make it seem more animated rather than the number all of a sudden seeing the number jump from one number to the next.
There just has to be a better way of achieving the same effect (or better) rather than pulling data from the DOM every 64 Milliseconds. There are tons of real-time graph's out on the web that achieve the same effect but I can't figure out how so I came up with my own and don't really think that they do it this way either. Any ideas??? I would only like to use pure Javascript with no libraries such as jQuery.
var x = setInterval(function() {
var left = parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, '')) / (parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, '')) + parseInt(window.getComputedStyle(p_bar_right).getPropertyValue('width').replace(/px/i, ''))) * 100;
var right = parseInt(window.getComputedStyle(p_bar_right).getPropertyValue('width').replace(/px/i, '')) / (parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, '')) + parseInt(window.getComputedStyle(p_bar_right).getPropertyValue('width').replace(/px/i, ''))) * 100;
p_bar_left.querySelector('.percent-value').innerText = left.toFixed(2) + '%';
document.querySelector('#blue').querySelector('.percent-amount').innerText = left.toFixed(2) + '%';
p_bar_right.querySelector('.percent-value').innerText = right.toFixed(2) + '%';
document.querySelector('#red').querySelector('.percent-amount').innerText = right.toFixed(2) + '%';
}, 64);
setTimeout(function() {
clearInterval(x)
}, 2000);
var good = document.querySelector('#good');
var bad = document.querySelector('#bad');
var p_bar_left = document.querySelector('#progressbar-left');
var p_bar_right = document.querySelector('#progressbar-right');
var counter_left = 0;
var counter_right = 0;
var percent_left = 0;
var percent_right = 0;
function changePercent(increment, which) {
if (which == 'left') {
counter_left += increment;
} else if (which == 'right') {
counter_right += increment;
} else {
throw "Don't know which value to increase.";
}
percent_left = (counter_left / (counter_left + counter_right)) * 100;
percent_right = (counter_right / (counter_left + counter_right)) * 100;
p_bar_left.style.width = percent_left + '%';
p_bar_right.style.width = percent_right + '%';
document.querySelector('#total-amount').innerText = counter_right + counter_left;
var x = setInterval(function() {
var left = parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, '')) / (parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, '')) + parseInt(window.getComputedStyle(p_bar_right).getPropertyValue('width').replace(/px/i, ''))) * 100;
var right = parseInt(window.getComputedStyle(p_bar_right).getPropertyValue('width').replace(/px/i, '')) / (parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, '')) + parseInt(window.getComputedStyle(p_bar_right).getPropertyValue('width').replace(/px/i, ''))) * 100;
p_bar_left.querySelector('.percent-value').innerText = left.toFixed(2) + '%';
document.querySelector('#blue').querySelector('.percent-amount').innerText = left.toFixed(2) + '%';
p_bar_right.querySelector('.percent-value').innerText = right.toFixed(2) + '%';
document.querySelector('#red').querySelector('.percent-amount').innerText = right.toFixed(2) + '%';
}, 64);
setTimeout(function() {
clearInterval(x)
}, 2000);
}
good.addEventListener('click', function() {
changePercent(1, 'left');
});
bad.addEventListener('click', function() {
changePercent(1, 'right');
});
var tooltip = document.querySelectorAll('.tooltip');
var tooltipelement = document.querySelectorAll('#progressbar-left, #progressbar-right');
for (var x = tooltipelement.length; x--;) {
tooltipelement[x].addEventListener('mousemove', function(e) {
for (var i = tooltip.length; i--;) {
tooltip[i].style.left = e.pageX + 20 + 'px';
tooltip[i].style.top = e.pageY + 'px';
}
});
}
#progressbar-container {
display: flex;
position: relative;
width: 50vw;
height: 32px;
border: 2px solid black;
background-color: #ccc;
justify-content: space-between;
}
#progressbar-left {
position: relative;
height: 100%;
background-color: blue;
transition: width 2s;
align-items: center;
justify-content: center;
}
#progressbar-right {
position: relative;
height: 100%;
background-color: red;
transition: width 2s;
align-items: center;
justify-content: center;
}
.tooltip {
display: none;
position: fixed;
width: auto;
height: auto;
padding: 6px;
background-color: black;
text-align: center;
border-radius: 6px;
z-index: 1;
}
.object {
display: inline-block;
color: #fff;
}
.percent-value {
display: inline-block;
color: #fff;
}
#progressbar-left:hover .tooltip {
display: block;
}
#progressbar-right:hover .tooltip {
display: block;
}
#total {
display: block;
font-weight: bold;
}
#total-amount {
display: inline-block;
font-weight: normal;
}
#blue,
#red {
display: block;
font-weight: bold;
}
.percent-amount {
display: inline-block;
font-weight: normal;
}
<body>
<input type="button" value="Good" id="good">
<input type="button" value="Bad" id="bad">
<div id="progressbar-container">
<div id="progressbar-left">
<div class="tooltip">
<span class="tooltiptext">
<span class="object">Blue</span>
<span class="percent-value"></span>
</span>
</div>
</div>
<div id="progressbar-right">
<div class="tooltip">
<span class="tooltiptext">
<span class="object">Red</span>
<span class="percent-value"></span>
</span>
</div>
</div>
</div>
<span id="total">Total: <p id="total-amount">0</p></span>
<span id="blue">Percent Blue: <p class="percent-amount">0%</p></span>
<span id="red">Percent Red: <p class="percent-amount">0%</p></span>
</body>
JsFiddle