Here's Mozilla's code for handling touch gestures, such as two finger pinch/zoom, etc.
https://github.com/mdn/dom-examples/blob/main/touchevents/Multi-touch_interaction.html
And here is the live demo:
https://mdn.github.io/dom-examples/touchevents/Multi-touch_interaction.html
(This will only work on mobile—or at least it doesn't work on my Macbook Chrome browser.
I'm trying to replicate the same output using React. Here's what I have:
import React, { useState, useEffect } from 'react';
const TouchEventsContainer = () => {
const [logEvents, setLogEvents] = useState(false);
const [tpCache, setTpCache] = useState([]);
const [logText, setLogText] = useState("");
const [testText, setTestText] = useState("Tap Swipe");
const enableLog = () => {
setLogEvents(!logEvents);
setTestText(logText);
};
const clearLog = () => setLogText('');
const log = (name, ev, printTargetIds) => {
let s = `${name}: touches = ${ev.touches.length} ; targetTouches = ${ev.targetTouches.length} ; changedTouches = ${ev.changedTouches.length}`;
if (printTargetIds) {
s += "";
for (var i = 0; i < ev.targetTouches.length; i++) {
s += `... id = ${ev.targetTouches[i].identifier} <br>`;
}
}
setLogText((prevLogText) => prevLogText + s + "<br>");
};
const updateBackground = (ev) => {
switch (ev.targetTouches.length) {
case 1:
ev.target.style.background = "yellow";
break;
case 2:
ev.target.style.background = "pink";
break;
default:
ev.target.style.background = "lightblue";
}
}
const handlePinchZoom = (ev) => {
if (ev.targetTouches.length === 2 && ev.changedTouches.length === 2) {
let point1 = -1, point2 = -1;
for (let i = 0; i < tpCache.length; i++) {
if (tpCache[i].identifier === ev.targetTouches[0].identifier) point1 = i;
if (tpCache[i].identifier === ev.targetTouches[1].identifier) point2 = i;
}
if (point1 >= 0 && point2 >= 0) {
let diff1 = Math.abs(tpCache[point1].clientX - ev.targetTouches[0].clientX);
let diff2 = Math.abs(tpCache[point2].clientX - ev.targetTouches[1].clientX);
const PINCH_THRESHHOLD = ev.target.clientWidth / 10;
if (diff1 >= PINCH_THRESHHOLD && diff2 >= PINCH_THRESHHOLD)
ev.target.style.background = "green";
} else {
setTpCache([]);
}
}
}
const startHandler = (ev) => {
ev.preventDefault();
if (ev.targetTouches.length === 2) {
const newTpCache = [...tpCache];
for (let i = 0; i < ev.targetTouches.length; i++) {
newTpCache.push(ev.targetTouches[i]);
}
setTpCache(newTpCache);
}
if (logEvents) {
log("touchStart", ev, true);
}
updateBackground(ev);
};
const moveHandler = (ev) => {
ev.preventDefault();
if (logEvents) {
log("touchMove", ev, false);
}
if (!(ev.touches.length === 2 && ev.targetTouches.length === 2)) {
updateBackground(ev);
}
ev.target.style.outline = "dashed";
handlePinchZoom(ev);
};
const endHandler = (ev) => {
ev.preventDefault();
if (logEvents) {
log(ev.type, ev, false);
}
if (ev.targetTouches.length === 0) {
ev.target.style.background = "white";
ev.target.style.outline = "1px solid black";
}
};
const setHandlers = (name) => {
const el = document.getElementById(name);
el.ontouchstart = startHandler;
el.ontouchmove = moveHandler;
el.ontouchcancel = endHandler;
el.ontouchend = endHandler;
};
useEffect(() => {
setHandlers("target1");
setHandlers("target2");
setHandlers("target3");
setHandlers("target4");
}, []);
return (
<>
<h1>Multi-touch interaction</h1>
<div id="target1">{testText}</div>
<div id="target2"> Tap, Hold or Swipe me 2</div>
<div id="target3"> Tap, Hold or Swipe me 3</div>
<div id="target4"> Tap, Hold or Swipe me 4</div>
<button id="log" onClick={() => enableLog()}>Start/Stop event logging</button>
<button id="clearlog" onClick={() => clearLog()}>Clear the log</button>
<p></p>
<div>{logText}</div>
</>
);
};
export default TouchEventsContainer;
I have 2 problems:
My log output (currently in a ) doesn't produce anything. I think it's because my string contains HTML. I tried using "dangerousInnerHTML" and some other things, but to no avail.
The two finger pinch-zoom (when background color turns green) doesn't seem to be detected.
What is wrong with my React conversion?
Also I'm currently rendering my TouchEventsContainer within the context of a different JS view (on my app) for testing, but I don't think that's the issue.
Related
I have a problem with my code
So, there is picture with like, when i click on the heart that add a like
there is a container down ma page and then the likes are also displayed in it.
(picture to show you a context)
The problem is everything work fine, but when you use my function "filter"
the likes are working on the photos but not down the page and that return me an error i don't really understand from where that come.
So can you help me please ?
import showMethods from "../factories/showMethods.js";
import lightbox from "../factories/lightbox.js";
import formularSecur from "../factories/formularSecur.js";
export default class OnePhotographer {
constructor() {
this.photographer = null;
this.medias = [];
this.likes = 0;
this.lightbox = lightbox;
this.indexLightbox = 0;
this.getPhotographer();
this.eInit();
}
eInit() {
document.getElementById("filtre").addEventListener("change", (e) => {
this.filter(e.target.value);
});
document.getElementById("filtre").addEventListener("click", (e) => {
this.filter(e.target.value);
});
document.getElementById("contactForm").addEventListener("submit", (e) => {
e.preventDefault();
formularSecur.checkForm(this);
document.getElementById("contactForm").reset();
});
this.lightbox.init(this);
document.getElementById("closeLightbox").addEventListener("click", () => {
this.lightbox.closeLightbox();
});
document.getElementById("prevBox").addEventListener("click", () => {
this.lightbox.navLightbox("prev");
});
document.getElementById("nextBox").addEventListener("click", () => {
this.lightbox.navLightbox("next");
});
document.addEventListener("keydown", (event) => {
if (event.code == "ArrowLeft") {
this.lightbox.navLightbox("prev");
} else if (event.code == "ArrowRight") {
this.lightbox.navLightbox("next");
} else if (event.code == "Escape") {
this.lightbox.closeLightbox();
}
});
}
filter(param) {
if (param === "popular") {
this.medias = this.medias.sort(this.order(param));
} else {
this.medias = this.medias.sort(this.order(param)).reverse();
}
document.querySelector(".containerMedias").innerHTML = "";
this.medias.forEach((media, index) => {
showMethods.showMedia(media, index);
showMethods.showLikes(this, instance);
});
}
order(settings) {
return function (x, y) {
if (x[settings] < y[settings]) {
return -1;
}
if (x[settings] > y[settings]) {
return 1;
}
return 0;
};
}
setMedias(media) {
this.medias = media;
}
async getPhotographer() {
const response = await fetch("./data/photographers.json");
const res = await response.json();
const id = this.idUrlCatch();
res.photographers.forEach((photographer) => {
if (photographer.id == id) {
this.photographer = photographer;
res.media.forEach((media) => {
if (media.photographerId == this.photographer.id) {
this.medias.push(media);
this.likes += media.likes;
}
});
this.medias = this.medias.sort(this.order("popular"));
}
});
showMethods.showData(this);
showMethods.showLikes(this, instance);
}
idUrlCatch() {
const str = window.location.href;
const url = new URL(str);
if (url.searchParams.get("id")) {
const id = url.searchParams.get("id");
return id;
}
}
showData() {
showMethods.showData(this);
showMethods.showLikes(this, instance);
}
showHeader() {
showMethods.showHeader(this);
}
showMedia(media, index) {
showMethods.showMedia(media, index, this);
}
showLikes() {
showMethods.showLikes(this, instance);
}
setSrcLightbox() {
showMethods.setSrcLightbox(this);
}
closeLightbox() {
showMethods.closeLightbox(this);
}
navLightbox() {
}
}
this is the second part of my code
import lightbox from "../factories/lightbox.js";
export default {
showData: function (instance) {
this.showHeader(instance);
instance.medias.forEach((media, index) => {
this.showMedia(media, index, instance);
});
instance.showLikes();
},
showHeader: function (instance) {
const infoHeader = document.querySelector(".photograph-header .containerInfo");
const title = document.createElement("h1");
title.textContent = instance.photographer.name;
const loc = document.createElement("h3");
loc.textContent = `${instance.photographer.city}, ${instance.photographer.country}`;
const tagline = document.createElement("span");
tagline.textContent = instance.photographer.tagline;
infoHeader.appendChild(title);
infoHeader.appendChild(loc);
const headerImg = document.querySelector(
".photograph-header .containerImg"
);
const pic = `assets/photographers/${instance.photographer.portrait}`;
const img = document.createElement("img");
img.setAttribute("src", pic);
img.setAttribute("alt", instance.photographer.name);
headerImg.appendChild(img);
},
showMedia: function (media, index, instance) {
const mediaSection = document.querySelector(".containerMedias");
const card = document.createElement("div");
card.classList.add("cardMedia");
const containerImg = document.createElement("div");
containerImg.classList.add("containerImg");
if (media.image) {
const mediaUrl = `assets/medias/${media.image}`;
const mediaItem = document.createElement("img");
mediaItem.dataset.index = index;
mediaItem.setAttribute("tabindex", 0);
mediaItem.addEventListener("click", (e) => {
lightbox.showLightbox(e, instance);
});
mediaItem.addEventListener("focus", () => {
mediaItem.addEventListener("keydown", (e) => {
if (e.code == "Enter") {
lightbox.showLightbox(e, instance);
}
});
});
mediaItem.setAttribute("src", mediaUrl);
mediaItem.setAttribute("alt", media.title);
containerImg.appendChild(mediaItem);
} else {
const mediaUrl = `assets/medias/${media.video}`;
const mediaItem = document.createElement("video");
mediaItem.dataset.index = index;
mediaItem.controls = true;
mediaItem.classList.add("media");
mediaItem.addEventListener("click", (e) => {
lightbox.showLightbox(e, instance);
});
mediaItem.setAttribute("src", mediaUrl);
mediaItem.setAttribute("alt", media.title);
mediaItem.setAttribute("data-index", index);
containerImg.appendChild(mediaItem);
}
const subContain = document.createElement("div");
subContain.classList.add("subContain");
const picTitle = document.createElement("h3");
picTitle.textContent = media.title;
const like = document.createElement("span");
like.setAttribute("tabindex", 0);
like.classList.add("like");
like.innerHTML = `${media.likes} <i class="fa-regular fa-heart"></i>`;
like.addEventListener("click", () => {
like.classList.toggle("active");
if (like.classList.contains("active")) {
media.likes += 1;
instance.likes += 1;
like.innerHTML = `${media.likes} <i class="fa-solid fa-heart"></i>`;
} else {
media.likes -= 1;
instance.likes -= 1;
like.innerHTML = `${media.likes} <i class="fa-regular fa-heart"></i>`;
}
instance.showLikes();
});
like.addEventListener("focus", () => {
like.addEventListener("keydown", (event) => {
if (event.code == "Enter") {
like.classList.toggle("active");
if (like.classList.contains("active")) {
media.likes += 1;
instance.likes += 1;
like.innerHTML = `${media.likes} <i class="fa-solid fa-heart"></i>`;
} else {
media.likes -= 1;
instance.likes -= 1;
like.innerHTML = `${media.likes} <i class="fa-regular fa-heart"></i>`;
}
instance.showLikes();
}
});
});
subContain.appendChild(picTitle);
subContain.appendChild(like);
card.appendChild(containerImg);
card.appendChild(subContain);
mediaSection.appendChild(card);
},
showLikes: function (instance) {
const priceContainer = document.createElement("div");
priceContainer.classList.add("priceContainer");
const likes = document.createElement("span");
likes.classList.add("likes");
likes.innerHTML = `<i class="fa-solid fa-heart"></i> ${instance.likes}`;
const priceDay = document.createElement("span");
priceDay.classList.add("priceDay");
priceDay.innerHTML = `<i class="fa-solid fa-dollar-sign"></i> ${instance.photographer.price}`;
priceContainer.appendChild(likes);
priceContainer.appendChild(priceDay);
document.querySelector(".photograph-header").appendChild(priceContainer);
},
};
i don't really get why with my function showMedia everithing is fine, but with showLikes not
the code error
Uncaught TypeError: instance.showLikes is not a function
at HTMLSpanElement.<anonymous> (showMethods.js:101:16)
(anonyme) # showMethods.js:101
if you help me that will save my life
Thank you for your time <3
So i find why the bug is happening when i use my "filter" Function.
I lose Instance on "showLikes" Function, so i don't have accès to the current instance.
One of my solution make my code in the onePhotograph.js, so no Instance ==> No bug.
I know this is maybe not the only solution, and maybe not the best. But this is a great solution also !
Best Regards
I have two events drag start and drag end. At the time of drag start I am deciding whether to move it or copy depending on some logic and setSelectedElement is running and setting the new element to it but as you can see console of new element in drag end and console for previously selected in drag start are both empty.
after some debugging, I found out it is turning to an empty string supplied in the very beginning of use state.
App.js
export default function App() {
const [selectedElement, setSelectedElement] = useState("");
const [diffX, setDiffX] = useState(0);
const [diffY, setDiffY] = useState(0);
const [group, setGroup] = useState([]);
useEffect(() => {
console.log("selected element changed");
}, [selectedElement]);
const handleDragStart = (event) => {
console.log("drag start ");
// console.log("class list", event.currentTarget.classList);
console.log("previous selected element is ", selectedElement);
let functionalityType = "";
let elementSelected = "";
let classList = event.currentTarget.classList;
for (let i = 0; i < classList.length; i++) {
//move element
if (classList[i].includes("group")) {
functionalityType = "move";
break;
}
}
if (functionalityType !== "move") {
console.log("inside copy");
elementSelected = event.currentTarget.cloneNode(true);
elementSelected.style.position = "absolute";
elementSelected.addEventListener("dragstart", handleDragStart);
elementSelected.addEventListener("dragend", handleDragEnd);
} else {
console.log("inside move");
elementSelected = event.currentTarget;
// console.log("event current target", event.currentTarget);
}
setDiffX(event.clientX - event.currentTarget.getBoundingClientRect().left);
setDiffY(event.clientY - event.currentTarget.getBoundingClientRect().top);
setSelectedElement(elementSelected);
};
const handleDragEnd = (event) => {
console.log("drag end");
let newElement = selectedElement;
console.log("new element is", newElement);
newElement.style.top = event.clientY - diffY + "px";
newElement.style.left = event.clientX - diffX + "px";
document.getElementById("MidArea").appendChild(newElement);
}
return (
<div
draggable={true}
onDragStart={props.handleDragStart}
onDragEnd={props.handleDragEnd}
className="draggable"
>);
}
Is there any reason you're doing setSelectedElement(elementSelected); outside else statement
Your code lacks some details to debug and find the missing piece however, drag and drop, I had this implemented sometime back, try the below
const stopDrag = () => {
document.onmouseup = null;
document.onmousemove = null;
};
export const dragComponent = (dragRef) => {
let coordinateOne = 0;
let coordinateTwo = 0;
let coordinateThree = 0;
let coordinateFour = 0;
const dragHandle = dragRef.current;
const elementDrag = (event) => {
event.preventDefault();
coordinateOne = coordinateThree - event.clientX;
coordinateTwo = coordinateFour - event.clientY;
coordinateThree = event.clientX;
coordinateFour = event.clientY;
dragHandle.style.top = `${dragHandle.offsetTop - coordinateTwo}px`;
dragHandle.style.left = `${dragHandle.offsetLeft - coordinateOne}px`;
};
const dragDown = (event) => {
event.preventDefault();
coordinateThree = event.clientX;
coordinateFour = event.clientY;
document.onmouseup = stopDrag;
document.onmousemove = elementDrag;
};
dragHandle.onmousedown = dragDown;
};
function MyComponent(){
const dragRef = useRef();
useEffect(() => {
dragComponent(dragRef);
}, []);
return (
<div draggable ref={dragRef}>
<h1> I am draggable..drag me around</h1>
</div>
)
}
Did you try this:
setDiffX(event.clientX - event.currentTarget.getBoundingClientRect().left);
setDiffY(event.clientY - event.currentTarget.getBoundingClientRect().top);
dragElementRef.current = elementSelected;
setSelectedElement(elementSelected);
};
let dragElementRef = useRef(selectedElement);
....
....
const handleDragEnd = (event) => {
//setTimeout(() => {
console.log("drag end");
let newElement = dragElementRef.current;
console.log("new element is", newElement);
newElement.style.top = event.clientY - diffY + "px";
newElement.style.left = event.clientX - diffX + "px";
document.getElementById("MidArea").appendChild(newElement);
//},500);
}
What I think is happening is your handleDragEnd() is being called before the next render cycle with updated value of state exists.
Try adding a setTimeout to add some delay.
I have a custom cursor component based off Codrops https://tympanus.net/codrops/2019/01/31/custom-cursor-effects/.
I have it working nicely on every page except one, which is a portfolio page that dynamically shows/hides content based on the current state.
If I change states (show component not initially loaded + hide initial component OR hide component not initially loaded + show initially loaded content), the custom cursor no longer reacts to the class name.
I am showing/hiding the content based on the current state of the component.
I've tried to re "init" the cursor, but that causes problems with the size/position of the custom cursor.
I understand the Cursor.js doesn't know when to "look" for the new classes after the state change, but I can't figure out how to make it "watch" for the changes, and since re initializing the cursor isn't working, I'm not sure what I should try. Should I try an eventListener that forces the cursor to look at the classes again?
Ideally the custom cursor will interact with both dynamic and non-dynamic elements, but I'm at a point that I question if this is even possible.
Any advice would be greatly appreciated. Or if anyone has an example of a React Custom Cursor that works in this scenario, I'd love to check it out.
Portfolio.js
render() {
return (
<div>
<div className="cursor">
<div className="circle-cursor circle-cursor--inner"></div>
</div>
<canvas
className="circle-cursor circle-cursor--outer"
resize="true"
></canvas>
//CURSOR
<CustomCursor />
//SHOW ALL PROJECTS ON STATE CHANGE
{ this.state.showAllItems ?
<div className={styles.grid}>
{list.map(item => (
<div key={item.index} className={styles.grid__item}>
<a href="#/a" key={item.index} value={item.index} onClick={e => this.onClick(e, item)} >
<div key={item.index} className={styles.glitch + " portfolioLinkHover " + styles.glitchStyle1}>
<div className={styles.glitch__img + " img"} style={{background: 'url('+ item.imgSrc + ') no-repeat 50% 0'}}></div>
<div className={styles.glitch__img + " img"} style={{background: 'url('+ item.imgSrc + ') no-repeat 50% 0'}}></div>
</div>
</a>
<h2>{item.itemTitle} <span>{item.itemSubtitle}</span></h2>
</a>
</p>
</div>
))}
</div>
: null }
//SHOW PROJECT ON STATE CHANGE
{ this.state.currentProject ?
<div className={styles.projectContainer + " portfolioLinkHover"}>
<Project />
</div> : null }
</div>
);
}
Cursor.js
import React, {Component} from 'react';
import Util from "./utils/util";
import './styles/cursor.scss';
import TweenMax from 'gsap';
class CustomCursor extends Component{
constructor(props) {
super(props);
this.state = { isOn: this.props.isOn };
}
componentDidMount(){
if(this.state.isOn == "true"){
this.initHovers();
}
else{
this.initCursor();
this.initCanvas();
this.initHovers();
}
}
initCursor() {
this.clientX = -100;
this.clientY = -100;
this.innerCursor = document.querySelector(".circle-cursor--inner");
this.outerCursor = document.querySelector(".custom-cursor--outer");
this.outerCursorSpeed = 1;
this.lastX = 0;
this.lastY = 0;
this.isStuck = false;
this.showCursor = false;
const { paper } = window;
const unveilCursor = e => {
this.group.position = new paper.Point(e.clientX, e.clientY);
setTimeout(() => {
this.outerCursorSpeed = 0.2;
}, 100);
this.showCursor = true;
};
document.addEventListener("mousemove", unveilCursor);
document.addEventListener("mousemove", e => {
this.clientX = e.clientX;
this.clientY = e.clientY;
});
const render = () => {
TweenMax.set(this.innerCursor, {
x: this.clientX,
y: this.clientY
});
if (this.showCursor) {
document.removeEventListener("mousemove", unveilCursor);
}
requestAnimationFrame(render);
};
requestAnimationFrame(render);
}
initCanvas() {
const { paper, SimplexNoise } = window;
const canvas = document.querySelector(".circle-cursor--outer");
const shapeBounds = {
width: 75,
height: 75
};
paper.setup(canvas);
const strokeColor = "rgba(255, 0, 0, 0.5)";
const strokeWidth = 1;
const segments = 8;
const radius = 25;
const noiseScale = 150; // speed
const noiseRange = 4; // range of distortion
let isNoisy = false;
const polygon = new paper.Path.RegularPolygon(
new paper.Point(0, 0),
segments,
radius
);
polygon.strokeColor = strokeColor;
polygon.strokeWidth = strokeWidth;
polygon.smooth();
this.group = new paper.Group([polygon]);
this.group.applyMatrix = false;
const noiseObjects = polygon.segments.map(() => new SimplexNoise());
let bigCoordinates = [];
paper.view.onFrame = event => {
if (!this.isStuck) {
// move circle around normally
this.lastX = Util.lerp(this.lastX, this.clientX, this.outerCursorSpeed);
this.lastY = Util.lerp(this.lastY, this.clientY, this.outerCursorSpeed);
this.group.position = new paper.Point(this.lastX, this.lastY);
} else if (this.isStuck) {
// fixed position on a nav item
this.lastX = Util.lerp(this.lastX, this.stuckX, this.outerCursorSpeed);
this.lastY = Util.lerp(this.lastY, this.stuckY, this.outerCursorSpeed);
this.group.position = new paper.Point(this.lastX, this.lastY);
}
if (this.isStuck && polygon.bounds.width < shapeBounds.width) {
// scale up the shape
polygon.scale(1.08);
} else if (!this.isStuck && polygon.bounds.width > 30) {
// remove noise
if (isNoisy) {
polygon.segments.forEach((segment, i) => {
segment.point.set(bigCoordinates[i][0], bigCoordinates[i][1]);
});
isNoisy = false;
bigCoordinates = [];
}
// scale down the shape
const scaleDown = 0.92;
polygon.scale(scaleDown);
}
// while stuck and when big, do perlin noise
if (this.isStuck && polygon.bounds.width >= shapeBounds.width) {
isNoisy = true;
// first get coordinates of large circle
if (bigCoordinates.length === 0) {
polygon.segments.forEach((segment, i) => {
bigCoordinates[i] = [segment.point.x, segment.point.y];
});
}
// calculate noise value for each point at that frame
polygon.segments.forEach((segment, i) => {
const noiseX = noiseObjects[i].noise2D(event.count / noiseScale, 0);
const noiseY = noiseObjects[i].noise2D(event.count / noiseScale, 1);
const distortionX = Util.map(noiseX, -1, 1, -noiseRange, noiseRange);
const distortionY = Util.map(noiseY, -1, 1, -noiseRange, noiseRange);
const newX = bigCoordinates[i][0] + distortionX;
const newY = bigCoordinates[i][1] + distortionY;
segment.point.set(newX, newY);
});
}
// hover state for main nav items
if (this.fillOuterCursor && polygon.fillColor !== strokeColor) {
polygon.fillColor = strokeColor;
polygon.strokeColor = "transparent";
} else if (!this.fillOuterCursor && polygon.fillColor !== "transparent") {
polygon.strokeColor = "rgba(255, 0, 0, 0.5)";
polygon.fillColor = "transparent";
}
// hover state for portfolio nav items
if (this.isOnPortfolioItem) {
polygon.fillColor = strokeColor;
polygon.strokeColor = "rgba(255, 226, 0, 0.5)";
// scale up the shape
polygon.bounds.width = 45;
polygon.bounds.height = 45;
} else if (!this.fillOuterCursor) {
polygon.strokeColor = "rgba(255, 0, 0, 0.5)";
polygon.fillColor = "transparent";
}
polygon.smooth();
};
}
initHovers() {
const handleMouseEnter = e => {
const navItem = e.currentTarget;
const navItemBox = navItem.getBoundingClientRect();
this.stuckX = Math.round(navItemBox.left + navItemBox.width / 2);
this.stuckY = Math.round(navItemBox.top + navItemBox.height / 2);
this.isStuck = true;
};
const handleMouseLeave = () => {
this.isStuck = false;
};
const linkItems = document.querySelectorAll(".browser-window__link");
linkItems.forEach(item => {
item.addEventListener("mouseenter", handleMouseEnter);
item.addEventListener("mouseleave", handleMouseLeave);
});
const mainNavItemMouseEnter = () => {
this.outerCursorSpeed = 0.8;
this.fillOuterCursor = true;
};
const mainNavItemMouseLeave = () => {
this.outerCursorSpeed = 0.2;
this.fillOuterCursor = false;
};
const portfolioItems = document.querySelectorAll(".portfolioLinkHover");
portfolioItems.forEach(item => {
item.addEventListener("mouseenter", handleLinkEnter);
item.addEventListener("mouseleave", handleLinkLeave);
});
}
render() {
return (
<div></div>
);
}
}
export default CustomCursor;
I've been battle with this problem for 2 days now, I'm trying to implement a continuous vertical text scrolling, but for some reasons unknown to me react returns a number instead of my array object (after I logged the output to see what was going wrong with my code) at the second iteration (as the array is constantly updated at interval). For the record, this was initially implemented in an Angular JS application, but I'm trying to convert it to react with useEffect() and useState() to update the changes in the array.
Below is what I've done so far:
const Skills = () => {
let skillWrap = useRef();
let [activeSkills, setActiveSkills] = useState([]);
console.log(activeSkills);
console.log(typeof activeSkills);
let sMax = totalSkills.length - 1; // 0 -> 19
let activeStart = Math.floor(Math.random() * (sMax + 1));
let activeEnd = activeStart === 0 ? sMax : activeStart - 1;
for (let e = activeStart; e <= sMax; e++) {
setActiveSkills(activeSkills.push(totalSkills[e]));
}
if (activeStart !== 0) {
for (let s = 0; s <= activeEnd; s++) {
setActiveSkills(activeSkills.push(totalSkills[s]));
}
}
let scrollDis = 0,
scrollingDown = false,
scrollingUp = false,
scrollingDownSelf = false,
scrollingUpSelf = false,
scrollCatchInterval = 40,
scrollDirection = "up",
hasScrolledRecently = false;
const wheelEventHandler = e => {
let skillFocused = skillWrap.current.childNodes[19];
skillFocused.classList.remove("active");
function animateScroll(scrollDis, callback) {
let curLeftTop = scrollDis * 8,
curLeftFinal = scrollDis * 4;
tween(skillWrap.current, -curLeftFinal, curLeftTop, 1, callback);
}
function scrollUp() {
setTimeout(() => {
for (let su = 0; su < scrollDis; su++) {
activeEnd--;
activeStart--;
if (activeEnd < 0) activeEnd = 19;
if (activeStart < 0) activeStart = 19;
/*setActiveSkills(activeSkills.unshift(totalSkills[activeStart]));
setActiveSkills(activeSkills.pop());*/
activeSkills.unshift(totalSkills[activeStart]);
activeSkills.pop();
}
skillFocused.classList.add("active");
skillWrap.current.style.transform = "none";
scrollDis = 0;
scrollingUp = false;
scrollingUpSelf = false;
if (e.deltaZ === 0) {
setTimeout(() => {
hasScrolledRecently = false;
}, 3000);
}
}, 0);
}
function scrollDown() {
setTimeout(() => {
for (let sd = 0; sd < Math.abs(scrollDis); sd++) {
activeEnd++;
activeStart++;
if (activeEnd > 19) activeEnd = 0;
if (activeStart > 19) activeStart = 0;
/*setActiveSkills(activeSkills.push(totalSkills[activeEnd]));
setActiveSkills(activeSkills.shift());*/
activeSkills.push(totalSkills[activeEnd]);
activeSkills.shift();
}
skillFocused.classList.add("active");
skillWrap.style.transform = "none";
scrollDis = 0;
scrollingDown = false;
scrollingDownSelf = false;
if (e.deltaZ === 0) {
setTimeout(() => {
hasScrolledRecently = false;
}, 3000);
}
}, 0);
}
if (
(e.deltaY === 100 || e.deltaY === 3) &&
!scrollingUp &&
!scrollingDownSelf
) {
// (scroll down) add skill to bottom & remove skill from top
scrollDirection = "down";
scrollDis--;
scrollingDown = true;
if (e.deltaZ === 0) hasScrolledRecently = true;
let scd = scrollDis;
setTimeout(() => {
if (scrollDis === scd) {
if (scrollDis < -6) scrollDis = -6;
scrollingDownSelf = true;
animateScroll(scrollDis, scrollDown);
}
}, scrollCatchInterval);
} else if (
(e.deltaY === -100 || e.deltaY === -3) &&
!scrollingDown &&
!scrollingUpSelf
) {
// (scroll up) add skill to top & remove skill from bottom
scrollDirection = "up";
scrollDis++;
scrollingUp = true;
if (e.deltaZ === 0) hasScrolledRecently = true;
let scu = scrollDis;
setTimeout(() => {
if (scrollDis === scu) {
if (scrollDis > 5) scrollDis = 5;
scrollingUpSelf = true;
animateScroll(scrollDis, scrollUp);
}
}, scrollCatchInterval);
}
};
function tween(o, x, y, durationSecs, onComplete) {
let fps = 30,
count = 0,
stopAt = fps * durationSecs,
easef = Quad_easeInOut;
let f = function() {
count++;
if (count >= stopAt) {
tween_stop(o);
if (onComplete) onComplete();
} else {
tween_setProperty(
o,
easef(count, 0, x, stopAt),
easef(count, 0, y, stopAt)
);
}
};
clearInterval(o._tween_int);
o._tween_int = setInterval(f, (durationSecs * 1000) / fps);
}
function tween_stop(o) {
clearInterval(o._tween_int);
}
function tween_setProperty(o, x, y) {
o.style.cssText += ";transform:translate3d(" + x + "vw," + y + "vh,0);";
}
function Quad_easeInOut(t, b, c, d) {
if ((t /= d / 2) < 1) return (c / 2) * t * t * t * t + b;
return (-c / 2) * ((t -= 2) * t * t * t - 2) + b;
}
useEffect(() => {
/*console.log(activeSkills);*/
setTimeout(() => {
skillWrap.current.childNodes[19].classList.add("active");
}, 2000);
window.addEventListener("wheel", wheelEventHandler);
function constantScroll() {
// prevents scrolling while changing views
setTimeout(function() {
// emulate scrolling of the skill list
let scrollEvent = new WheelEvent("wheel", {
deltaY: scrollDirection === "up" ? -100 : 100,
deltaZ: 1 // used to differentiate between user scroll / programmatic scroll
});
if (!hasScrolledRecently) {
// 3 scroll events are dispatched to mirror scrolling of 3 skills
for (let r = 0; r < 3; r++) {
window.dispatchEvent(scrollEvent);
}
}
constantScroll();
}, 3000);
}
// wait 3 seconds before issuing first scroll
setTimeout(function() {
constantScroll();
}, 2000);
return () => {
window.removeEventListener("wheel", wheelEventHandler);
console.log("Skills Component will unmount");
};
}, [activeSkills]);
return (
<div>
<div className="view skills active active-f">
<div className="header-container skills">
<div className="header-title-wrap">
<div className="cover" />
<h1 className="header big first">My</h1>
<h1 className="header big last">Skillsset</h1>
</div>
<div className="header-info-wrap">
<div className="header-content-body skill-one">
<div className="line-left" />
<p className="header body about-one">
The core of my skill set largely surrounds the MERN{" "}
<strong>stack</strong>, to achieve both a clear and dynamic user
experience. I also have some experience with mobile integration
(Android and iOS). Strengthening my grasp of the mobile app
development space is one of my primary goals for the near
future.
</p>
</div>
</div>
</div>
<div className="skill-container active active-f">
<div className="skill-wrap">hey</div>
</div>
</div>
<div className="skill-container active-f active">
<div ref={skillWrap} className="skill-wrap">
{console.log(activeSkills)}
{activeSkills.map((skill, i) => (
<div key={i} className="skill">
{skill}
</div>
))}
</div>
</div>
</div>
);
};
Below is the https://codesandbox.io/ page to see what I've done so far, and the link to the Angular JS implementation which I'm trying to achieve with React.
My React implementation (from the skills route): https://codesandbox.io/s/shy-http-jlrle
Original Angular implementation (skills route): https://codesandbox.io/s/github/eazylaykzy/portfolio-ex
I really would appreciate any effort at helping me get this to work.
Just to abstract the first part as mentioned in my comment, you could write your own hook and use it as so
import React, { useMemo } from 'react';
// How to shuffle an array
// https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
function shuffle(array) {
const newArray = [...array];
for (let i = newArray.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
}
function useActiveSkills(skills) {
return useMemo(() => shuffle(skills), [skills]);
}
function Component() {
const activeSkills = useActiveSkills();
return (
<>
{activeSkills.map((skill) => (
<p>{skill.name}</p>
))}
</>
);
}
I have an anchor tag that plays a sound everytime the link is hovered:
<a onmouseenter="playAudio();">LINK</a>
However, when hovered, the link creates some characters that, if I keep moving the mouse inside the element, it keeps playing the sound over and over again.
Is there a way to play the onmouseenter function only once until it has detected the event onmouseout?
I've tried adding:
<a onmouseenter="playAudio();this.onmouseenter = null;">LINK</a>
but then it only plays the sound once.
Here goes the TextScramble animation function:
// ——————————————————————————————————————————————————
// TextScramble
// ——————————————————————————————————————————————————
class TextScramble {
constructor(el) {
this.el = el
this.chars = '!<>-_\\/[]{}—=+*^?#________'
this.update = this.update.bind(this)
}
setText(newText) {
const oldText = this.el.innerText
const length = Math.max(oldText.length, newText.length)
const promise = new Promise((resolve) => this.resolve = resolve)
this.queue = []
for (let i = 0; i < length; i++) {
const from = oldText[i] || ''
const to = newText[i] || ''
const start = Math.floor(Math.random() * 40)
const end = start + Math.floor(Math.random() * 40)
this.queue.push({
from,
to,
start,
end
})
}
cancelAnimationFrame(this.frameRequest)
this.frame = 0
this.update()
return promise
}
update() {
let output = ''
let complete = 0
for (let i = 0, n = this.queue.length; i < n; i++) {
let {
from,
to,
start,
end,
char
} = this.queue[i]
if (this.frame >= end) {
complete++
output += to
} else if (this.frame >= start) {
if (!char || Math.random() < 0.28) {
char = this.randomChar()
this.queue[i].char = char
}
output += `<span class="dud">${char}</span>`
} else {
output += from
}
}
this.el.innerHTML = output
if (complete === this.queue.length) {
this.resolve()
} else {
this.frameRequest = requestAnimationFrame(this.update)
this.frame++
}
}
randomChar() {
return this.chars[Math.floor(Math.random() * this.chars.length)]
}
}
// ——————————————————————————————————————————————————
// Configuration
// ——————————————————————————————————————————————————
const phrases = [
'MAIN_HUB'
]
const el = document.querySelector('.link_mainhub')
const fx = new TextScramble(el)
let counter = 0
const next = () => {
fx.setText(phrases[counter]).then(() => {});
}
el.addEventListener('mouseenter', next);
<a class="link_mainhub" onmouseenter="playAudio();">LINK</a>
Thanks.
Set a variable when you start playing the audio, and check for this before playing it again. Have the animation unset the variable when it's done.
function audioPlaying = false;
function playAudio() {
if (!audioPlaying) {
audioPlaying = true;
audioEl.play();
}
}
const next = () => {
fx.setText(phrases[counter]).then(() => {
audioPlaying = false;
});
}