<div class="container">
<div id="content"></div>
</div>
* {
padding: 0;
margin: 0;
user-select: none;
}
body {
background-color: #efefef;
overflow: hidden;
}
.container {
display: flex;
justify-content: center;
align-items: center;
transform: scale(.80);
position: relative;
}
.display {
width: 375px;
height: 667px;
background-color: #3F51B5;
border-radius: 14px;
}
.apps {
z-index: 1;
position: absolute;
top: 0;
padding: 24px;
padding-top: 28px;
}
.homerow-bg {
background-color: #222;
width: 375px;
height: 96px;
z-index: 0;
position: absolute;
top: 571px;
border-radius: 0 0 14px 14px;
}
.app-icon {
position: absolute;
border: 1px solid black;
border-radius: 14px;
width: 60px;
height: 60px;
}
const { Motion, spring, presets } = ReactMotion;
const reinsert = (arr, from, to) => {
// shallow copy of the array
const result = arr.slice();
// value to reinsert
const val = result[from];
// remove the value from the result
result.splice(from, 1);
// reinsert the value into the result
result.splice(to, 0, val);
return result;
};
const clamp = (n, min, max) => {
return Math.max(Math.min(n, max), min);
};
const colors = [
'#EF767A', '#456990', '#49BEAA', '#49DCB1', '#EEB868', '#EF767A', '#456990',
'#49BEAA', '#49DCB1', '#EEB868', '#EF767A',
];
const getColors = (n) => {
return _.times(n, () => {
// random index less than the length
const index = Math.floor(Math.random() * colors.length);
return colors[index];
});
};
const springSetting1 = {stiffness: 180, damping: 10};
const springSetting2 = {stiffness: 120, damping: 17};
const [count, width, height] = [28, 88, 88];
const appColors = getColors(28);
// indexed by visual position
const layout = _.times(count, (n) => {
const row = Math.floor(n / 4);
const col = n % 4;
if (n > 23) {
return [width * col, (height + 5) * row];
}
return [width * col, height * row];
});
class App extends React.Component {
state = {
mouseXY: [0, 0],
mouseCircleDelta: [0, 0],
lastPress: null,
isPressed: false,
order: _.times(count, Number)
};
componentDidMount() {
window.addEventListener('touchmove', this.handleTouchMove);
window.addEventListener('touchend', this.handleMouseUp);
window.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('mouseup', this.handleMouseUp);
};
handleTouchStart = (key, pressLocation, e) => {
this.handleMouseDown(key, pressLocation, e.touches[0]);
};
handleTouchMove = (e) => {
e.preventDefault();
this.handleMouseMove(e.touches[0]);
};
handleMouseMove = ({pageX, pageY}) => {
// destructure getting the current values from this state
const {order, lastPress, isPressed, mouseCircleDelta: [dx, dy]} = this.state;
if (isPressed) {
const mouseXY = [pageX - dx, pageY - dy];
const col = clamp(Math.floor(mouseXY[0] / width), 0, 3);
const row = clamp(Math.floor(mouseXY[1] / height), 0, 6);
const index = row * 4 + col;
const newOrder = reinsert(order, order.indexOf(lastPress), index);
this.setState({mouseXY, order: newOrder});
}
};
handleMouseDown = (key, [pressX, pressY], {pageX, pageY}) => {
this.setState({
lastPress: key,
isPressed: true,
mouseCircleDelta: [pageX - pressX, pageY - pressY],
mouseXY: [pressX, pressY]
});
};
handleMouseUp = () => {
this.setState({
isPressed: false,
mouseCircleDelta: [0, 0]
})
};
render() {
const {order, lastPress, isPressed, mouseXY} = this.state;
return (
<div className="display">
<div className="homerow-bg"/>
<div className="apps">
{order.map((_, key) => {
let style;
let x;
let y;
const visualPosition = order.indexOf(key);
if (key === lastPress && isPressed) {
[x, y] = mouseXY;
style = {
translateX: x,
translateY: y,
scale: spring(1.2, springSetting1),
boxShadow: spring((x - (3 * width - 50) / 2) / 15, springSetting1)
};
} else {
[x, y] = layout[visualPosition];
style = {
translateX: spring(x, springSetting2),
translateY: spring(y, springSetting2),
scale: spring(1, springSetting1),
boxShadow: spring(0, springSetting1),
};
}
return (
<Motion key={key} style={style}>
{ interpolatedStyle => {
return (
<div
onMouseDown={this.handleMouseDown.bind(null, key, [x, y])}
className="app-icon"
style={{
backgroundColor: appColors[key],
transform: `translate3d(
${interpolatedStyle.translateX}px,
${interpolatedStyle.translateY}px,
0
)
scale(${interpolatedStyle.scale})`,
zIndex: key === lastPress ? 99 : visualPosition,
boxShadow: `${interpolatedStyle.boxShadow}px 5px 5px rgba(0,0,0,0.5)`,
}}
/>
)
}
}
</Motion>
)
})}
</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("content"));
View Compiled
This Pen doesn't use any external CSS resources.