IPhone / IPad : Vanilla JS scrollTop isn't working on Chrome - javascript

Vue CLI 3 , Vue 2.5.17
I want to scroll to Specific div element when RestApi response is success. But my code isn't working to IOS devices.
I try to "scroll", "scrollTo" and "scrollTop" functions. But those functions aren't working.
const el = document.querySelector(selector)
// Custom Scroll Class Method
Scroll.scrollIt(el, 200, 'linear')
// Common ScrollTo Method
document.querySelector('html', 'body')
.scrollTo({
top: (el.offsetTop - 90),
left: 0,
behavior: 'smooth'
})
ScrollIt function is a custom Scroll class method.
export default class Scroll {
public static scrollIt (element: HTMLElement, duration: number, callback?: Function) {
const start = document.querySelector('document, html, body').scrollTop
const change = element.offsetTop - start
let currentTime = 0
const increment = 20
const animateScroll = function () {
currentTime += increment
const val = easeInOutQuad(currentTime, start, change, duration)
document.querySelector('document, html, body').scrollTop = val - 90
if (currentTime < duration) {
setTimeout(animateScroll, increment)
}
}
animateScroll()
// t = current time
// b = start value
// c = change in value
// d = duration
function easeInOutQuad (t: number, b: number, c: number, d: number) {
t /= d/2
if (t < 1) return c/2*t*t + b
t--
return -c/2 * (t*(t-2) - 1) + b
}
}
}

Related

CLI-Progress package - How to hide the progress bar on start?

I'm using the CLI-Progress package from:
https://www.npmjs.com/package/cli-progress.
This is my implementation according to the documentation example:
https://github.com/npkgz/cli-progress/blob/master/examples/example-visual.js)
const b1 = new progress.Bar({
format: colors.cyan('[{bar}]') + ' {percentage}% || {value}/{total} Chunks || Speed: {speed}',
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
hideCursor: true,
});
b1.start(200, 0, {
speed: "N/A"
});
let value = 0;
const speedData: number[] = [];
const timer = setInterval(() => {
value++;
speedData.push(Math.random() * 2 + 5);
const currentSpeedData = speedData.splice(-10);
b1.update(value, {
speed: (currentSpeedData.reduce((a, b) => {
return a + b;
}, 0) / currentSpeedData.length).toFixed(2) + "Mb/s"
});
if (value >= b1.getTotal()) {
clearInterval(timer);
b1.stop();
}
}, 20);
Which renders :
I have two questions about this :
Why is there two bars (I would like to get rid of the first one) ?
Why does it work since the timer function is never called (it is called recursively but there is no first call) ?
Thank you.

useAnimationOnDidUpdate react hook implementation

So I want to use requestAnimationFrame to animate something using react hooks.
I want something small so react-spring/animated/react-motion is a bad choice for me.
I implemented useAnimationOnDidUpdate but it is working incorrectly, here is reproduction with details.
What's wrong here: on second click multiplier for animation starts with 1, but should always start with 0 (simple interpolation from 0 to 1).
So I'm trying to understand why the hook saved previous value though I started a new animation loop already.
Here is a full code listing for hook:
import { useState, useEffect, useRef } from 'react';
export function useAnimationOnDidUpdate(
easingName = 'linear',
duration = 500,
delay = 0,
deps = []
) {
const elapsed = useAnimationTimerOnDidUpdate(duration, delay, deps);
const n = Math.min(1, elapsed / duration);
return easing[easingName](n);
}
// https://github.com/streamich/ts-easing/blob/master/src/index.ts
const easing = {
linear: n => n,
elastic: n =>
n * (33 * n * n * n * n - 106 * n * n * n + 126 * n * n - 67 * n + 15),
inExpo: n => Math.pow(2, 10 * (n - 1)),
inOutCubic: (t) => t <.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
};
export function useAnimationTimerOnDidUpdate(duration = 1000, delay = 0, deps = []) {
const [elapsed, setTime] = useState(0);
const mountedRef = useRef(false);
useEffect(
() => {
let animationFrame, timerStop, start, timerDelay;
function onFrame() {
const newElapsed = Date.now() - start;
setTime(newElapsed);
if (newElapsed >= duration) {
console.log('>>>> end with time', newElapsed)
return;
}
loop();
}
function loop() {
animationFrame = requestAnimationFrame(onFrame);
}
function onStart() {
console.log('>>>> start with time', elapsed)
start = Date.now();
loop();
}
if (mountedRef.current) {
timerDelay = delay > 0 ? setTimeout(onStart, delay) : onStart();
} else {
mountedRef.current = true;
}
return () => {
clearTimeout(timerStop);
clearTimeout(timerDelay);
cancelAnimationFrame(animationFrame);
};
},
[duration, delay, ...deps]
);
return elapsed;
}
This problem with this hook is that it doesn't clean up the elapsedTime upon completion.
You can resolve this by adding setTime(0) to you onFrame function when the animation is expected to stop.
Like this:
function onFrame() {
const newElapsed = Date.now() - start;
if (newElapsed >= duration) {
console.log('>>>> end with time', newElapsed)
setTime(0)
return;
}
setTime(newElapsed);
loop();
}
I know it may seem weird that it doesn't reset itself. But bear in mind that your animation is making use of the same hook instance for both easing in and out. Therefore that cleanup is necessary.
Note: I've also move the setTime(newElapsed) line so that it's after the if statement since this isn't necessary if the if statement is true.
UPDATE:
To further improve how this works, you could move the setTime(0) to the return cleanup.
This would mean that you're onFrame function changes to:
function onFrame() {
const newElapsed = Date.now() - start;
if (newElapsed >= duration) {
console.log('>>>> end with time', newElapsed)
setTime(0)
return;
}
setTime(newElapsed);
loop();
}
And then update your return cleanup for useAnimationTimerOnDidUpdate to:
return () => {
clearTimeout(timerStop);
clearTimeout(timerDelay);
cancelAnimationFrame(animationFrame);
setTime(0);
};
I'm assuming that the reason your animation "isn't working properly" was because the component would flash. As far as my testing goes, this update fixes that.

Correct draggable element's pixel position to start from a arbitrary corner

The element containing a draggable or moveable handle can only start from the top left position, I have no idea how to adjust, calculate or correct the pixel values to make the draggable handle element start from say the bottom left like a normal graph would.
The position of the handle is used to generate a range restricted value like a percentage value between 0-100 for both the x and y axis regardless of the element's pixel size.
It's a range-input or position picker of sorts intended for use in a color picker widget.
The color gradients change depending on the widget's relative position to the left, top or right of something, hence the picker or handle should adjust the starting point of it's range accordingly.
I'm using onpointermove to get the x and y positions of the div.handle and
adjust for the relative width, height, left and top, offsets of the parent element.
What I cannot figure out for the life of me is the math and code needed to allow the range input to track the position from an arbitrary corner, preferably bottom left.
Sorry for using a custom library but this example is mostly vanilla, at least the calculations which matter are.
const {dom, component, each, on, once, isNum, $, run} = rilti
// keep a number between a minimum and maximum ammount
const clamp = (n, min, max) => Math.min(Math.max(n, min), max)
// define element behavior
component('range-input', {
// set up everything before element touches DOM
create (range /* Proxy<Function => Element> */) {
// setup zero values in state (observer-like abstraction tracking changes)
range.state({value: 0, valueX: 0, valueY: 0})
// local vars for easier logic
let Value, ValueY
// create element <div class="handle"> and append to <range-input>
// also add property to range and get it as a const
const handle = range.handle = dom.div.handle({$: range})
// set the range limits at 0-100% by default for X and Y axis
if (range.limit == null) range.limitX = range.limit = 100
if (range.limit !== range.limitX) range.limitX = range.limit
if (range.limitY == null) range.limitY = range.limit
// set the X position by percentage/range number,
// move the handle accordingly and change state
range.setX = (value = range.value || 0, skipChecks) => {
if (!skipChecks && value === Value) return
if (value > range.limitX || value < 0) throw new Error('value out of range')
// if the element is not in the dom
// then wait for it to mount first
if (!range.mounted) {
range.once.mount(e => range.setX(value))
return
}
// allow float values or round it to ints by default
if (!range.decimals) value = Math.round(value)
const hWidth = handle.offsetWidth
// get pixel range
const Min = hWidth / 2
const Max = range.offsetWidth - Min
// calculate pixel postion from range value
const hLeft = (value / range.limitX) * (Max - Min)
handle.style.left = hLeft + 'px'
// update all the states
Value = range.state.value = range.state.valueX = value
}
// same as setX but for Y axis
range.setY = (value = range.valueY || 0, skipChecks) => {
if (!skipChecks && value === Value) return
if (value > range.limitY || value < 0) throw new Error('value out of range')
if (!range.mounted) {
range.once.mount(e => range.setY(value))
return
}
const hHeight = handle.offsetHeight
const Min = hHeight / 2
const Max = range.offsetHeight - Min
const hTop = (value / range.limitY) * (Max - Min)
handle.style.top = hTop + 'px'
if (!range.decimals) value = Math.round(value)
ValueY = range.state.valueY = value
}
// get the raw Element/Node and define (s/g)etters
Object.defineProperties(range() /* -> <range-input> */, {
value: {get: () => Value, set: range.setX},
valueX: {get: () => Value, set: range.setX},
valueY: {get: () => ValueY, set: range.setY}
})
let rWidth // range.offsetWidth
let rHeight // range.offsetHeight
let rRect // cache of range.getBoundingClientRect()
// called when user moves the handle
const move = (x = 0, y = 0) => {
// check the the axis is not locked
// for when you want to use range-input as a slider
if (!range.lockX) {
// adjust for relative position
if (x < rRect.left) x = rRect.left
else if (x > rRect.left + rWidth) x = rRect.left + rWidth
x -= rRect.left
const hWidth = handle.offsetWidth
// get pixel range
const min = hWidth / 2
const max = rWidth - min
// keep it inside the block
const hLeft = clamp(x, min, max) - min
handle.style.left = hLeft + 'px'
// pixel position -> percentage/value
let value = (hLeft * range.limitX) / (max - min)
// round value to an int by default
if (!range.decimals) value = Math.round(value)
// set it if it's not the same as the old value
if (value !== Value) {
Value = range.state.value = range.state.valueX = value
}
}
// now do below as above for Y axis
if (!range.lockY) { // when it's not locked
if (y < rRect.top) y = rRect.top
else if (y > rRect.top + rWidth) y = rRect.top + rHeight
y -= rRect.top
const hHeight = handle.offsetHeight
const min = hHeight / 2
const max = range.offsetHeight - min
const hTop = clamp(y, min, max) - min
handle.style.top = hTop + 'px'
let value = (hTop * range.limitY) / (max - min)
if (!range.decimals) value = Math.round(value)
if (value !== ValueY) {
ValueY = range.state.valueY = value
}
}
// .dispatchEvent(new CustomEvent('input'))
range.emit('input')
// call an update function if it's present as a prop
if (range.update) range.update(range, handle)
}
// track and manage starting, stopping and moving events
// for .pointer(up/down/move) event types respectively.
const events = range.state.events = {
move: on.pointermove(document, e => move(e.x, e.y)).off(),
stop: on.pointerup(document, () => {
events.move.off()
events.start.on()
}).off(),
start: once.pointerdown([range, handle], () => {
[rWidth, rHeight] = [range.offsetWidth, range.offsetHeight]
rRect = range.getBoundingClientRect()
events.move.on()
events.stop.on()
}).off()
}
// ^-- all the events are off at the start
// they get turned on when the element mounts
},
// when Element enters DOM set the positions
mount (range) {
if (!range.lockY) range.handle.style.top = 0
range.setX()
range.setY()
// start listening for user interactions
range.state.events.start.on()
},
// start listening again on DOM re-entry
remount (range) {
range.state.events.start.on()
},
// stop listening when removed from DOM
unmount ({state: {events}}) { each(events, e => e.off()) },
// track custom attribute to set some props conveniently
attr: {
opts (range, val) {
run(() => // wait for DOMContentLoaded first
val.split(';')
.filter(v => v != null && v.length)
.map(pair => pair.trim().split(':').map(part => part.trim()))
.forEach(([prop, value]) => {
if (value.toLowerCase() === 'true') value = true
else if (value.toLowerCase() === 'false') value = false
else {
const temp = Number(value)
if (isNum(temp)) value = temp
}
if (prop === 'x' || prop === 'v') {
range.setX(value, true)
} else if (prop === 'y') {
range.setY(value, true)
} else {
range[prop] = value
}
})
)
}
}
})
// show the values of the range-input
$('span.stats').append($('range-input').state`
X: ${'valueX'}%, Y: ${'valueY'}%
`)
// add a title
dom.h4('<range-input>: custom element').prependTo('body')
range-input {
position: relative;
display: block;
margin: 1em auto;
width: 250px;
height: 250px;
border: 1px solid #ccc;
}
range-input > div.handle {
position: absolute;
background: #ccc;
width: 20px;
height: 20px;
cursor: grab;
user-drag: none;
user-select: none;
touch-action: none;
}
.details {
width: 225px;
text-align: left;
margin: 3em auto;
}
* {
box-sizing: border-box;
}
body {
text-align: center;
color: hsl(0,0%,40%);
}
h4 {
margin: 0 auto;
}
<range-input opts="x: 35; y: 80;"></range-input>
<span class="stats"></span>
<section class="details">
<p>
<b>Please Help:</b><br>
I can't figure out how to code it so that
the range-input could start at an arbitrary corner
instead of just top left.
I'd like it to start counting from bottom left instead.
</p>
<pre style="text-align: left;"><code>
// the handle should be able to start at
left: 0;
bottom: 0;
// with X/Y being zero;
// not sure how to achieve this.
</code></pre>
</section>
<script src="https://rawgit.com/SaulDoesCode/rilti.js/experimental/dist/rilti.js"></script>
Same example on
Codepen
Another way to describe your issue is that your y-axis goes from 100 (technically, limitY) to 0 when it ought to go from 0 to 100. Therefore, we can slightly change your code to reverse this axis by fully calculating the y percentage, and then subracting it from 100. (ie, 100 - 80 = 20 or 100 - 35 - 65.) This will change the high values into low values and vice versa. Then, if we want to convert from percentage to pixel, we simply subtract it from 100 again to get our original flipped percentage (that you've already done all of the work for.)
The two lines changed are:
const hTop = (value / range.limitY) * (Max - Min)
becomes
const hTop = (1 - value / range.limitY) * (Max - Min)
// 1 - value / range.limitY is a shortening of (range.limitY - value) / range.limitY
and
let value = (hTop * range.limitY) / (max - min)
becomes
let value = range.limitY * (1 - hTop / (max - min))
// this is also a shortening, you could have written it,
// value = range.limitY - (hTop * range.limitY) / (max - min)
Here's the Codepen.
Likewise, if you want to flip the x axis, you can use similar logic on that part of the code. You can flip various combinations of the two axes to start at various corners.
A harder version of the same problem (a good exercise for practice) is how to properly convert from your pixel, not just to a percentage, but also to any range a to b, with b possibly being smaller than a.

Functional Programming: Calling a Curried Function

I'm implementing the game Tic Tac Toe/Naughts and Crosses in a functional programming style and have stumbled across a hurdle with curried functions.
I have a reoccurring pattern of functions in the form func(width, height, index) which I then wish to curry, binding width and height and leaving curriedFunc(index).
However the problem arises when I have functions that expect one of these curried functions to be defined at compile-time.
They cannot be defined at compile time, because they need input from the user to then bind the values to the function.
Below is some example code of the pattern I've encountered.
// Board indexes:
// 0 | 1 | 2
// ---+---+---
// 3 | 4 | 5
// ---+---+---
// 6 | 7 | 8
const getRowNumGivenWidth = w => i => Math.floor(i/w);
// I want to be able to declare nextIndexInRowGivenWidth() here, outside of main()
// but getRowNum() needs to be defined beforehand
const main = () => {
// User input:
const width = 3;
// ...
const getRowNum = getRowNumGivenWidth(width);
const nextIndexInRowGivenWidth = width => currentIndex => {
const rowNum = getRowNum(currentIndex);
const nextIndex = currentIndex + 1;
if (getRowNum(nextIndex) != rowNum)
result = nextIndex - width;
else
result = nextIndex;
return result;
};
const nextIndexInRow = nextIndexInRowGivenWidth(width);
const board = [0, 1, 2, 3, 4, 5, 6, 7, 8];
board.map(x => console.log(x, " -> ", nextIndexInRow(x)));
// ...
}
main();
The only way I can think of solving this is to pass the curried function as an argument (to nextIndexInRowGivenWidth() in this example).
However I don't think this is ideal as if a function requires a few similarly curried functions at run-time, it quickly becomes unwieldy to define and curry said function.
The ideal solution would be if I could somehow make the binding of the values dynamic, suppose I could put the declaration getRowNum = getRowNumGivenWidth(width); before main(). This way I could call something like getRowNum(someInt) to initialise getRowNum() which I could then use in other functions that are already expecting it to be defined.
As this is a reoccurring pattern in my code, I was wondering if there is a design pattern to achieve this.
I think you are looking for
const getRowNumGivenWidth = w => i => Math.floor(i/w);
const nextIndexInRowGivenWidth = width => {
const getRowNum = getRowNumGivenWidth(width);
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
return currentIndex => {
const nextIndex = currentIndex + 1;
if (getRowNum(nextIndex) != getRowNum(currentIndex))
return nextIndex - width;
else
return nextIndex;
};
};
const main = () => {
// User input:
const width = 3;
const nextIndexInRow = nextIndexInRowGivenWidth(width);
// ...
}
Alternatively, you could define that nextIndexInRowGiven… function not with the width as the first curried parameter, but with getRowNum itself as the parameter:
const getRowNumGivenWidth = w => i => Math.floor(i/w);
const nextIndexInRowGivenRowNumGetter = getRowNum => currentIndex => {
const nextIndex = currentIndex + 1;
if (getRowNum(nextIndex) != getRowNum(currentIndex))
return nextIndex - width;
else
return nextIndex;
};
const main = () => {
// User input:
const width = 3;
const nextIndexInRow = nextIndexInRowGivenRowNumGetter(getRowNumGivenWidth(width));
// ...
}

Easing animations in Canvas

Trying to create a function that lets you animate any number of numerical properties with a given easing function, but it doesn't quite work... calling it doesn't result in any motion. Everything is set up correctly as when I change what the values change to, it does show, so that means it's the equation that's the problem here. It's either not giving the right value, or not getting the right ones.
function animate(obj, props, options) {
var start = Date.now(),
total = start + options.duration,
diff = total - start,
vals = {},
id;
for (var v in props) {
vals[v] = props[v];
}
(function update() {
var curr = Date.now(),
progress = Math.min((options.duration - (total - curr)) / options.duration, 1);
for (var p in props) {
console.log(obj[p] = options.equation(curr, vals[p], obj[p] - vals[p], total));
}
if (progress < 1) {
id = requestAnimationFrame(update);
} else {
id = cancelAnimationFrame(id);
if (typeof options.callback === 'function') {
options.callback();
}
}
}());
}
animate(rect, {
x: map.width / 2,
y: map.height / 2
}, {
duration: 2000,
equation: function(t, b, c, d) {
return c * (t /= d) * t + b;
},
callback: function() {
console.log('Whoa... it works.'); // ...yeah, nope. ;(
}
});
t = time, b = beginning value, c = change in value, d = duration.
Am I giving it the wrong arguments? How would I make this work?
Your time & duration arguments should not be summed with Date.now().
If you want your easing to take 2000ms then send 2000 (d) into the easing equation.
The time to send into the easing equation is the elapsed time, so send Date.now()-startTime (t) into the easing equation.
I assume you have properly set the beginning value (b) and net change in value (c).

Categories

Resources