mirror of
https://github.com/Sped0n/bridget.git
synced 2026-04-14 10:09:31 -07:00
Gsap (#120)
* feat: add gsap to deps * chore: migrate to pnpm * refractor: change var name * chore(.idea): remove unnecessary files and configurations Remove the following files and configurations from the .idea directory: - .gitignore: Remove default ignored files. - bridget.iml: Remove module file. - codeStyles/Project.xml: Remove project code style configuration. - codeStyles/codeStyleConfig.xml: Remove project code style configuration. - inspectionProfiles/Project_Default.xml: Remove project inspection profile. - jsLibraryMappings.xml: Remove JavaScript library mappings. - jsLinters/eslint.xml: Remove ESLint configuration. - modules.xml: Remove project module configuration. - vcs.xml: Remove VCS directory mapping. - watcherTasks.xml: Remove project tasks options. * chore(.prettierrc.json): update Prettier configuration to include support for go-template files and improve code formatting settings The Prettier configuration file (.prettierrc.json) has been updated with the following changes: - `useTabs` is set to `false` to use spaces for indentation - `tabWidth` is set to `2` to specify the number of spaces for each indentation level - `printWidth` is set to `88` to limit the line length to 88 characters - `singleQuote` is set to `true` to use single quotes for strings - `trailingComma` is set to `none` to remove trailing commas in arrays and objects - `bracketSpacing` is set to `true` to add spaces inside brackets - `semi` is set to `false` to remove semicolons at the end of statements - `plugins` is added to include the "prettier-plugin-go-template" plugin for go-template files - `overrides` is added to specify the parser as "go-template" for files with the ".html" extension * chore(base.scss): improve font rendering by adding font smoothing properties to all elements feat(base.scss): add user-select: none to body to disable text selection feat(base.scss): add overscroll-behavior-y: none to html and body to disable vertical scrolling on overscroll feat(base.scss): add cursor: pointer to anchor tags and buttons for better user experience feat(font.scss): add font-face declaration for HelveticaNow font refactor(media.scss): remove unused file feat(mixins.scss): add min-width and max-width mixins for responsive design feat(reset.scss): add the new CSS reset version 1.8.4 feat(reset.scss): remove all styles from User-Agent-Stylesheet except for the display property feat(reset.scss): set box-sizing: border-box for all elements feat(reset.scss): revert cursor style for anchor tags and buttons feat(reset.scss): remove list styles (bullets/numbers) from ol, ul, and menu feat(reset.scss): set max-inline-size and max-block-size to 100% for images feat(reset.scss): set border-collapse: collapse for tables feat(reset.scss): set -webkit-user-select: auto for input and textarea to fix Safari issue feat(reset.scss): revert white-space property for textarea on Safari feat(reset.scss): set -webkit-appearance: revert for meter element feat(reset.scss): revert all styles for preformatted text feat(reset.scss): unset color for input placeholder feat(reset.scss): remove default dot sign for lists feat(reset.scss): set display: none for elements with hidden attribute feat(reset.scss): revert styles for contenteditable elements feat(reset.scss): set -webkit-user-drag: element for draggable elements feat(reset.scss): revert native behavior for modal dialogs feat(typography.scss): set line-height, font-size, and font-family for body feat(typography.scss): increase font-size for tablet and laptop breakpoints * feat: add custom cursor styles Add a new file `_customCursor.scss` to the `assets/css/_partial` directory. This file contains styles for a custom cursor. The `.cursor` class is used to position the cursor and set its appearance. The `.active` class is used to display the cursor. The `.cursorInner` class is used to position the inner content of the cursor. --- refactor: remove unused footer styles Delete the file `_footer.scss` from the `assets/css/_partial` directory. This file contains styles for the footer section of the page. The styles are no longer used and can be safely removed. --- refactor: remove unused image styles Delete the files `_imagesDesktop.scss` and `_imagesMobile.scss` from the `assets/css/_partial` directory. These files contain styles for displaying images on desktop and mobile devices. The styles are no longer used and can be safely removed. --- feat: add navigation styles Add a new file `_nav.scss` to the `assets/css/_partial` directory. This file contains styles for the navigation bar. The styles define the layout and appearance of the navigation bar, including its position, background color, and alignment of its contents. The styles also include media queries to adjust the layout for smaller screens. --- refactor: remove unused overlay styles Delete the file `_overlay.scss` from the `assets/css/_partial` directory. This file contains styles for an overlay element. The styles are no longer used and can be safely removed. --- feat: add stage styles Add a new file `_stage.scss` to the `assets/css/_partial` directory. This file contains styles for the stage element, which is used to display images. The styles define the position and size of the stage, as well as the appearance of the images within it. --- feat: add stage navigation overlay styles Add a new file `_stageNav.scss` to the `assets/css/_partial` directory. This file contains styles for the stage navigation overlay. The styles define the position and size of the overlay, as well as its appearance and behavior. The overlay is used for navigation within the stage. * chore(variables.scss): update variable names and values for better readability and consistency chore(style.scss): reorganize import statements for better organization and readability * fix(main.ts): import correct functions from utils module feat(stage.ts): implement stage navigation functionality feat(stageNav.ts): implement stage navigation overlay functionality feat(state.ts): implement state management for index and threshold feat(utils.ts): add utility functions for increment and decrement * fix(index.html): fix doctype declaration to use lowercase 'doctype' for HTML5 compliance fix(index.html): fix meta tag indentation for better readability fix(index.html): fix indentation of head and body tags for better readability fix(index.html): fix indentation of header, main, and footer sections for better readability fix(index.html): fix indentation of script tag for better readability fix(index.html): fix indentation of closing div tag for better readability fix(index.html): fix indentation of closing body and html tags for better readability feat(index.html): add partial for navigation bar to improve website navigation fix(head.html): fix indentation of esBuildOpts variable for better readability fix(head.html): fix indentation of script tag for better readability feat(nav.html): add navigation bar partial to improve website navigation * refactor(stage.ts): change cordHist, isOpen, isAnimating, and active variables to instances of the watchable class to improve semantics and encapsulation * refactor(customCursor.ts): rename addActiveCallback to active.addWatcher for better readability and consistency refactor(stageNav.ts): rename getIsAnimating to isAnimating.get for better readability and consistency refactor(stageNav.ts): rename addActiveCallback to active.addWatcher for better readability and consistency feat(stageNav.ts): add check for isOpen.get() and isAnimating.get() before handling key events to prevent unwanted actions * chore(package.json): add prettier-plugin-go-template as a dev dependency to enable formatting of Go templates with Prettier * chore: add helvetica now font * chore(tsconfig.json): add "moduleResolution" property with value "node" to improve module resolution in the project configuration * refactor(stage.ts): rename class 'watchable' to 'Watchable' for consistency and clarity feat(utils.ts): add Watchable class to provide a generic watchable object with getter, setter, and watcher functionality
This commit is contained in:
@@ -1,14 +1,20 @@
|
||||
html {
|
||||
font-family: $global-font-family;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
scroll-behavior: smooth;
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: white;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
line-height: 1.5;
|
||||
}
|
||||
user-select: none;
|
||||
background: white;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
6
assets/css/_core/_font.scss
Normal file
6
assets/css/_core/_font.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: HelveticaNow;
|
||||
src: url('/fonts/HelveticaNowText-Regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
28
assets/css/_core/_mixins.scss
Normal file
28
assets/css/_core/_mixins.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
$breakpoints: (
|
||||
'mobile': 375px,
|
||||
'tablet': 768px,
|
||||
'laptop': 1024px,
|
||||
'desktop': 1440px
|
||||
) !default;
|
||||
|
||||
// Breakpoints
|
||||
|
||||
@mixin min-width($breakpoint) {
|
||||
@if map-has-key($breakpoints, $breakpoint) {
|
||||
@media (min-width: map-get($breakpoints, $breakpoint)) {
|
||||
@content;
|
||||
}
|
||||
} @else {
|
||||
@error "Unfortunately, no value could be retrieved from `#{$breakpoint}`. " + "Available breakpoints are: #{map-keys($breakpoints)}.";
|
||||
}
|
||||
}
|
||||
|
||||
@mixin max-width($breakpoint) {
|
||||
@if map-has-key($breakpoints, $breakpoint) {
|
||||
@media (max-width: (map-get($breakpoints, $breakpoint) - 1px)) {
|
||||
@content;
|
||||
}
|
||||
} @else {
|
||||
@error "Unfortunately, no value could be retrieved from `#{$breakpoint}`. " + "Available breakpoints are: #{map-keys($breakpoints)}.";
|
||||
}
|
||||
}
|
||||
103
assets/css/_core/_reset.scss
Normal file
103
assets/css/_core/_reset.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
/***
|
||||
The new CSS reset - version 1.8.4 (last updated 14.2.2023)
|
||||
GitHub page: https://github.com/elad2412/the-new-css-reset
|
||||
***/
|
||||
|
||||
/*
|
||||
Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
|
||||
- The "symbol *" part is to solve Firefox SVG sprite bug
|
||||
*/
|
||||
*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
|
||||
all: unset;
|
||||
display: revert;
|
||||
}
|
||||
|
||||
/* Preferred box-sizing value */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Reapply the pointer cursor for anchor tags */
|
||||
a,
|
||||
button {
|
||||
cursor: revert;
|
||||
}
|
||||
|
||||
/* Remove list styles (bullets/numbers) */
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* For images to not be able to exceed their container */
|
||||
img {
|
||||
max-inline-size: 100%;
|
||||
max-block-size: 100%;
|
||||
}
|
||||
|
||||
/* removes spacing between cells in tables */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* Safari - solving issue when using user-select:none on the <body> text input doesn't working */
|
||||
input,
|
||||
textarea {
|
||||
-webkit-user-select: auto;
|
||||
}
|
||||
|
||||
/* revert the 'white-space' property for textarea elements on Safari */
|
||||
textarea {
|
||||
white-space: revert;
|
||||
}
|
||||
|
||||
/* minimum style to allow to style meter element */
|
||||
meter {
|
||||
-webkit-appearance: revert;
|
||||
appearance: revert;
|
||||
}
|
||||
|
||||
/* preformatted text - use only for this feature */
|
||||
:where(pre) {
|
||||
all: revert;
|
||||
}
|
||||
|
||||
/* reset default text opacity of input placeholder */
|
||||
::placeholder {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
/* remove default dot (•) sign */
|
||||
::marker {
|
||||
content: initial;
|
||||
}
|
||||
|
||||
/* fix the feature of 'hidden' attribute.
|
||||
display:revert; revert to element instead of attribute */
|
||||
:where([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* revert for bug in Chromium browsers
|
||||
- fix for the content editable attribute will work properly.
|
||||
- webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
|
||||
:where([contenteditable]:not([contenteditable='false'])) {
|
||||
-moz-user-modify: read-write;
|
||||
-webkit-user-modify: read-write;
|
||||
overflow-wrap: break-word;
|
||||
-webkit-line-break: after-white-space;
|
||||
-webkit-user-select: auto;
|
||||
}
|
||||
|
||||
/* apply back the draggable feature - exist only in Chromium and Safari */
|
||||
:where([draggable='true']) {
|
||||
-webkit-user-drag: element;
|
||||
}
|
||||
|
||||
/* Revert Modal native behavior */
|
||||
:where(dialog:modal) {
|
||||
all: revert;
|
||||
}
|
||||
14
assets/css/_core/_typography.scss
Normal file
14
assets/css/_core/_typography.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
@import 'mixins';
|
||||
|
||||
body {
|
||||
line-height: 1.2;
|
||||
font-size: 16px;
|
||||
font-family: HelveticaNow, helvetica, arial, sans-serif;
|
||||
|
||||
@include min-width('tablet') {
|
||||
font-size: 18px;
|
||||
}
|
||||
@include min-width('laptop') {
|
||||
font-size: 19px;
|
||||
}
|
||||
}
|
||||
21
assets/css/_partial/_customCursor.scss
Normal file
21
assets/css/_partial/_customCursor.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
.cursor {
|
||||
position: fixed;
|
||||
z-index: var(--z-cursor);
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: none;
|
||||
cursor: none;
|
||||
pointer-events: none;
|
||||
|
||||
color: white;
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
|
||||
.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cursorInner {
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
footer {
|
||||
max-height: fit-content;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 22;
|
||||
background-color: #fff;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
line-height: 1.5;
|
||||
padding: 4px 9px;
|
||||
float: none;
|
||||
|
||||
.footer_name {
|
||||
}
|
||||
|
||||
.footer_categoryWrapper {
|
||||
.footer_category {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selected {
|
||||
text-decoration: underline;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.footer_threshold {
|
||||
button {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
padding: 0 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.thid{
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer_imageIndex {
|
||||
margin: 0;
|
||||
|
||||
.ftid {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
width: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
footer {
|
||||
top: 0;
|
||||
padding: 3px 8px;
|
||||
font-size: 17px;
|
||||
height: 31px;
|
||||
|
||||
.footer_threshold {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.footer_imageIndex {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
.imagesDesktop {
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
object-fit: contain;
|
||||
max-height: 50vmin;
|
||||
max-width: 100vw;
|
||||
|
||||
&[data-status='null'] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&[data-status='top'] {
|
||||
opacity: 1;
|
||||
max-height: calc(100vh - var(--footer-height));;
|
||||
transition-property: transform, max-height;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 0.5s, 0.5s;
|
||||
}
|
||||
|
||||
&[data-status='trail'] {
|
||||
opacity: 0;
|
||||
margin-top: 40px;
|
||||
transition-property: opacity, margin-top;
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
&[data-status='resumeTop'] {
|
||||
opacity: 1;
|
||||
max-height: 50vmin;
|
||||
transition-property: max-height, transform;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 0.7s, 0.5s;
|
||||
}
|
||||
|
||||
&[data-status='resume'] {
|
||||
opacity: 1;
|
||||
margin-top: 0;
|
||||
transition-property: opacity, margin-top;
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
&[data-status='overlay'] {
|
||||
opacity: 1;
|
||||
max-height: calc(100vh - var(--footer-height));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
.imagesMobile {
|
||||
height: 100vh;
|
||||
overflow: scroll;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
height: 20vh;
|
||||
width: 60vw;
|
||||
object-fit: contain;
|
||||
position: sticky;
|
||||
top: 50vh;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 20vh;
|
||||
}
|
||||
}
|
||||
44
assets/css/_partial/_nav.scss
Normal file
44
assets/css/_partial/_nav.scss
Normal file
@@ -0,0 +1,44 @@
|
||||
@import '../_core/mixins';
|
||||
$tablet: map-get($breakpoints, 'tablet') - 1;
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
height: var(--nav-height);
|
||||
padding: 0 var(--space-standard);
|
||||
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
background: white;
|
||||
|
||||
z-index: var(--z-nav);
|
||||
|
||||
// Maintain functionality while container is locked
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.num {
|
||||
width: 0.625em;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.current {
|
||||
font-style: italic;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: $tablet), (hover: none) {
|
||||
nav {
|
||||
top: 0;
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
.index,
|
||||
.threshold {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
.overlay_cursor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
mix-blend-mode: difference;
|
||||
font-size: 19px;
|
||||
box-sizing: border-box;
|
||||
cursor: none;
|
||||
user-select: none;
|
||||
|
||||
.cursor_innerText {
|
||||
color: white;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
}
|
||||
}
|
||||
22
assets/css/_partial/_stage.scss
Normal file
22
assets/css/_partial/_stage.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.stage {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100vw;
|
||||
height: calc(var(--window-height) - var(--nav-height));
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100vw;
|
||||
height: var(--window-height);
|
||||
object-fit: contain;
|
||||
|
||||
transform: scale(0.6);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
21
assets/css/_partial/_stageNav.scss
Normal file
21
assets/css/_partial/_stageNav.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
.navOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--z-nav-gallery);
|
||||
|
||||
width: 100vw;
|
||||
height: calc(var(--window-height) - var(--nav-height));
|
||||
|
||||
display: flex;
|
||||
cursor: none;
|
||||
|
||||
&:not(.active) {
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
// ==============================
|
||||
// Variables
|
||||
// ==============================
|
||||
|
||||
// ========== Global ========== //
|
||||
// Font and Line Height
|
||||
$global-font-family: system-ui, -apple-system, BlinkMacSystemFont, PingFang SC, Microsoft YaHei UI, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, Helvetica, Arial, sans-serif !default;
|
||||
$global-font-size: 16px;
|
||||
$global-font-weight: 400;
|
||||
$global-line-height: 1.5rem;
|
||||
@import '_core/mixins';
|
||||
|
||||
:root {
|
||||
--footer-height: 38px;
|
||||
}
|
||||
--window-height: 100vh;
|
||||
--nav-height: 2rem;
|
||||
--space-standard: 0.625rem;
|
||||
|
||||
--z-curtain: 200;
|
||||
--z-nav-gallery: 500;
|
||||
--z-cursor: 600;
|
||||
--z-nav: 800;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
@charset "utf-8";
|
||||
|
||||
@import "_variables";
|
||||
@import '_core/reset';
|
||||
@import '_core/font';
|
||||
@import '_core/typography';
|
||||
@import '_core/mixins';
|
||||
@import '_variables';
|
||||
@import '_core/base';
|
||||
|
||||
@import "_core/base";
|
||||
|
||||
@import "_core/media";
|
||||
|
||||
@import "_partial/imagesDesktop";
|
||||
|
||||
@import "_partial/imagesMobile";
|
||||
|
||||
@import "_partial/footer";
|
||||
|
||||
@import "_partial/overlay";
|
||||
@import '_partial/customCursor';
|
||||
@import '_partial/nav';
|
||||
@import '_partial/stage';
|
||||
@import '_partial/stageNav';
|
||||
|
||||
38
assets/ts/customCursor.ts
Normal file
38
assets/ts/customCursor.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { active } from './stage'
|
||||
|
||||
let cursor: HTMLDivElement
|
||||
|
||||
// create cursor
|
||||
cursor = document.createElement('div')
|
||||
cursor.className = 'cursor'
|
||||
cursor.classList.add('active')
|
||||
// create cursor inner
|
||||
const cursorInner = document.createElement('div')
|
||||
cursorInner.className = 'cursorInner'
|
||||
// append cursor inner to cursor
|
||||
cursor.append(cursorInner)
|
||||
|
||||
function onMouse(e: MouseEvent) {
|
||||
const x = e.clientX
|
||||
const y = e.clientY
|
||||
cursor.style.transform = `translate3d(${x}px, ${y}px, 0)`
|
||||
}
|
||||
|
||||
export function initCustomCursor(): void {
|
||||
// append cursor to main
|
||||
document.getElementById('main')!.append(cursor)
|
||||
// bind mousemove event to window
|
||||
window.addEventListener('mousemove', onMouse)
|
||||
// add active callback
|
||||
active.addWatcher(() => {
|
||||
if (active.get()) {
|
||||
cursor.classList.add('active')
|
||||
} else {
|
||||
cursor.classList.remove('active')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function setCustomCursor(text: string): void {
|
||||
cursorInner.innerText = text
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { type ImageData } from './utils'
|
||||
|
||||
// fetch images info from JSON
|
||||
const imageArrayElement = document.getElementById('images_array') as HTMLScriptElement
|
||||
const rawImageArray = imageArrayElement.textContent as string
|
||||
export const imagesArray: ImageData[] = JSON.parse(rawImageArray).sort(
|
||||
(a: ImageData, b: ImageData) => {
|
||||
if (a.index < b.index) {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
)
|
||||
export const imagesArrayLen: number = imagesArray.length
|
||||
@@ -1,165 +0,0 @@
|
||||
import { overlayEnable } from './overlay'
|
||||
import {
|
||||
calcImageIndex,
|
||||
center,
|
||||
delay,
|
||||
mouseToTransform,
|
||||
pushIndex,
|
||||
type position,
|
||||
hideImage
|
||||
} from './utils'
|
||||
import { thresholdIndex, thresholdSensitivityArray } from './thresholdCtl'
|
||||
import { imgIndexSpanUpdate } from './indexDisp'
|
||||
import { imagesArrayLen } from './dataFetch'
|
||||
import { imagesDivNodes as images } from './elemGen'
|
||||
|
||||
// global index for "activated"
|
||||
export let globalIndex: number = 0
|
||||
// last position set as "activated"
|
||||
let last: position = { x: 0, y: 0 }
|
||||
export let trailingImageIndexes: number[] = []
|
||||
// only used in overlay disable, for storing positions temporarily
|
||||
export let transformCache: string[] = []
|
||||
// abort controller for enter overlay event listener
|
||||
let EnterOverlayClickAbCtl = new AbortController()
|
||||
// stack depth of images array
|
||||
export let stackDepth: number = 5
|
||||
export let lastStackDepth: number = 5
|
||||
|
||||
export const addEnterOverlayEL = (e: HTMLImageElement): void => {
|
||||
EnterOverlayClickAbCtl.abort()
|
||||
EnterOverlayClickAbCtl = new AbortController()
|
||||
e.addEventListener(
|
||||
'click',
|
||||
() => {
|
||||
void enterOverlay()
|
||||
},
|
||||
{
|
||||
passive: true,
|
||||
once: true,
|
||||
signal: EnterOverlayClickAbCtl.signal
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// activate top image
|
||||
const activate = (index: number, mouseX: number, mouseY: number): void => {
|
||||
addEnterOverlayEL(images[index])
|
||||
if (stackDepth !== lastStackDepth) {
|
||||
trailingImageIndexes.push(index)
|
||||
refreshStack()
|
||||
lastStackDepth = stackDepth
|
||||
}
|
||||
const indexesNum: number = pushIndex(
|
||||
index,
|
||||
trailingImageIndexes,
|
||||
stackDepth,
|
||||
images,
|
||||
imagesArrayLen
|
||||
)
|
||||
// set img position
|
||||
images[index].style.transform = mouseToTransform(mouseX, mouseY, true, true)
|
||||
images[index].dataset.status = 'null'
|
||||
// reset z index
|
||||
for (let i = indexesNum; i > 0; i--) {
|
||||
images[trailingImageIndexes[i - 1]].style.zIndex = `${i}`
|
||||
}
|
||||
images[index].style.visibility = 'visible'
|
||||
last = { x: mouseX, y: mouseY }
|
||||
}
|
||||
|
||||
// Compare the current mouse position with the last activated position
|
||||
const distanceFromLast = (x: number, y: number): number => {
|
||||
return Math.hypot(x - last.x, y - last.y)
|
||||
}
|
||||
|
||||
// handle mouse move
|
||||
export const handleOnMove = (e: MouseEvent): void => {
|
||||
// meet threshold
|
||||
if (
|
||||
distanceFromLast(e.clientX, e.clientY) >
|
||||
window.innerWidth / thresholdSensitivityArray[thresholdIndex]
|
||||
) {
|
||||
// calculate the actual index
|
||||
const imageIndex = calcImageIndex(globalIndex, imagesArrayLen)
|
||||
// show top image and change index
|
||||
activate(imageIndex, e.clientX, e.clientY)
|
||||
imgIndexSpanUpdate(imageIndex + 1, imagesArrayLen)
|
||||
// self increment
|
||||
globalIndexInc()
|
||||
}
|
||||
}
|
||||
|
||||
async function enterOverlay(): Promise<void> {
|
||||
// stop images animation
|
||||
window.removeEventListener('mousemove', handleOnMove)
|
||||
// get index array length
|
||||
const indexesNum: number = trailingImageIndexes.length
|
||||
for (let i = 0; i < indexesNum; i++) {
|
||||
// create image element
|
||||
const e: HTMLImageElement = images[trailingImageIndexes[i]]
|
||||
// cache images' position
|
||||
transformCache.push(e.style.transform)
|
||||
// set style for the images
|
||||
if (i === indexesNum - 1) {
|
||||
e.style.transitionDelay = `${0.1 * i + 0.2}s, ${0.1 * i + 0.2 + 0.5}s`
|
||||
e.dataset.status = 'top'
|
||||
center(e)
|
||||
} else {
|
||||
e.style.transitionDelay = `${0.1 * i}s`
|
||||
e.dataset.status = 'trail'
|
||||
}
|
||||
}
|
||||
// sleep
|
||||
await delay(stackDepth * 100 + 100 + 1000)
|
||||
// post process
|
||||
for (let i = 0; i < indexesNum; i++) {
|
||||
images[trailingImageIndexes[i]].style.transitionDelay = ''
|
||||
if (i === indexesNum - 1) {
|
||||
images[trailingImageIndexes[i]].dataset.status = 'overlay'
|
||||
} else {
|
||||
images[trailingImageIndexes[i]].style.visibility = 'hidden'
|
||||
}
|
||||
}
|
||||
// Offset previous self increment of global index (by handleOnMove)
|
||||
globalIndexDec()
|
||||
// overlay init
|
||||
overlayEnable()
|
||||
}
|
||||
|
||||
// initialization
|
||||
export const trackMouseInit = (): void => {
|
||||
window.addEventListener('mousemove', handleOnMove)
|
||||
}
|
||||
|
||||
export const globalIndexDec = (): void => {
|
||||
globalIndex--
|
||||
}
|
||||
|
||||
export const globalIndexInc = (): void => {
|
||||
globalIndex++
|
||||
}
|
||||
|
||||
export const emptyTransformCache = (): void => {
|
||||
transformCache = []
|
||||
}
|
||||
|
||||
export const emptyTrailingImageIndexes = (): void => {
|
||||
trailingImageIndexes = []
|
||||
}
|
||||
|
||||
export const setStackDepth = (newStackDepth: number): void => {
|
||||
if (stackDepth !== newStackDepth) {
|
||||
lastStackDepth = stackDepth
|
||||
stackDepth = newStackDepth
|
||||
}
|
||||
}
|
||||
|
||||
export const refreshStack = (): void => {
|
||||
const l: number = trailingImageIndexes.length
|
||||
if (stackDepth < lastStackDepth && l > stackDepth) {
|
||||
const times: number = l - stackDepth
|
||||
for (let i = 0; i < times; i++)
|
||||
hideImage(images[trailingImageIndexes.shift() as number])
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { imagesArray, imagesArrayLen } from './dataFetch'
|
||||
import { createImgElement } from './utils'
|
||||
|
||||
// get components of overlay
|
||||
export let overlayCursor: HTMLDivElement
|
||||
export let cursorInnerContent: HTMLDivElement
|
||||
export let imagesDivNodes: NodeListOf<HTMLImageElement>
|
||||
|
||||
const mainDiv = document.getElementById('main') as HTMLDivElement
|
||||
|
||||
const passDesktopElements = (): void => {
|
||||
overlayCursor = document
|
||||
.getElementsByClassName('overlay_cursor')
|
||||
.item(0) as HTMLDivElement
|
||||
cursorInnerContent = document
|
||||
.getElementsByClassName('cursor_innerText')
|
||||
.item(0) as HTMLDivElement
|
||||
imagesDivNodes = document.getElementsByClassName('imagesDesktop')[0]
|
||||
.childNodes as NodeListOf<HTMLImageElement>
|
||||
}
|
||||
|
||||
const passMobileElements = (): void => {
|
||||
imagesDivNodes = document.getElementsByClassName('imagesMobile')[0]
|
||||
.childNodes as NodeListOf<HTMLImageElement>
|
||||
}
|
||||
|
||||
const createCursorDiv = (): HTMLDivElement => {
|
||||
const cursorDiv: HTMLDivElement = document.createElement('div')
|
||||
cursorDiv.className = 'overlay_cursor'
|
||||
const innerTextDiv: HTMLDivElement = document.createElement('div')
|
||||
innerTextDiv.className = 'cursor_innerText'
|
||||
cursorDiv.appendChild(innerTextDiv)
|
||||
return cursorDiv
|
||||
}
|
||||
|
||||
export const createDesktopElements = (): void => {
|
||||
mainDiv.appendChild(createCursorDiv())
|
||||
const imagesDiv: HTMLDivElement = document.createElement('div')
|
||||
imagesDiv.className = 'imagesDesktop'
|
||||
for (let i = 0; i < imagesArrayLen; i++) {
|
||||
imagesDiv.appendChild(createImgElement(imagesArray[i]))
|
||||
}
|
||||
mainDiv.appendChild(imagesDiv)
|
||||
passDesktopElements()
|
||||
}
|
||||
|
||||
export const createMobileElements = (): void => {
|
||||
const imagesDiv: HTMLDivElement = document.createElement('div')
|
||||
imagesDiv.className = 'imagesMobile'
|
||||
for (let i = 0; i < imagesArrayLen; i++) {
|
||||
imagesDiv.appendChild(createImgElement(imagesArray[i]))
|
||||
}
|
||||
mainDiv.appendChild(imagesDiv)
|
||||
passMobileElements()
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { imagesArray, imagesArrayLen } from './dataFetch'
|
||||
import { preloadImage, calcImageIndex } from './utils'
|
||||
|
||||
let lastIndex: number = 0
|
||||
|
||||
export const preloader = (index: number): void => {
|
||||
if (lastIndex === index) {
|
||||
for (let i: number = -2; i <= 1; i++)
|
||||
preloadImage(imagesArray[calcImageIndex(index + i, imagesArrayLen)].url)
|
||||
} else if (lastIndex > index) {
|
||||
for (let i: number = 1; i <= 3; i++)
|
||||
preloadImage(imagesArray[calcImageIndex(index - i, imagesArrayLen)].url)
|
||||
} else {
|
||||
for (let i: number = 1; i <= 3; i++)
|
||||
preloadImage(imagesArray[calcImageIndex(index + i, imagesArrayLen)].url)
|
||||
}
|
||||
lastIndex = index
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { duper } from './utils'
|
||||
|
||||
// update index of displaying image
|
||||
export const imgIndexSpanUpdate = (numOne: number, numTwo: number): void => {
|
||||
// footer index number display module
|
||||
const footerIndexDisp = document.getElementsByClassName('ftid')
|
||||
const numOneString: string = duper(numOne)
|
||||
const numTwoString: string = duper(numTwo)
|
||||
for (let i: number = 0; i <= 7; i++) {
|
||||
const footerIndex = footerIndexDisp[i] as HTMLSpanElement
|
||||
if (i > 3) {
|
||||
footerIndex.innerText = numTwoString[i - 4]
|
||||
} else {
|
||||
footerIndex.innerText = numOneString[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,13 @@
|
||||
import { createDesktopElements, createMobileElements } from './elemGen'
|
||||
import { imgIndexSpanUpdate } from './indexDisp'
|
||||
import { trackMouseInit } from './desktop'
|
||||
import { thresholdCtlInit } from './thresholdCtl'
|
||||
import { imagesArrayLen } from './dataFetch'
|
||||
import { vwRefreshInit } from './overlay'
|
||||
import { preloader } from './imageCache'
|
||||
import { getDeviceType } from './utils'
|
||||
import { renderImages } from './mobile'
|
||||
import { initResources } from './resources'
|
||||
import { initState } from './state'
|
||||
import { initCustomCursor } from './customCursor'
|
||||
import { initNav } from './nav'
|
||||
import { initStage } from './stage'
|
||||
import { initStageNav } from './stageNav'
|
||||
|
||||
const desktopInit = (): void => {
|
||||
createDesktopElements()
|
||||
preloader(0)
|
||||
vwRefreshInit()
|
||||
imgIndexSpanUpdate(0, imagesArrayLen)
|
||||
thresholdCtlInit()
|
||||
trackMouseInit()
|
||||
}
|
||||
|
||||
const mobileInit = (): void => {
|
||||
createMobileElements()
|
||||
vwRefreshInit()
|
||||
imgIndexSpanUpdate(0, imagesArrayLen)
|
||||
renderImages()
|
||||
console.log('mobile')
|
||||
}
|
||||
|
||||
getDeviceType().desktop ? mobileInit() : desktopInit()
|
||||
initCustomCursor()
|
||||
const ijs = initResources()
|
||||
initState(ijs.length)
|
||||
initStage(ijs)
|
||||
initStageNav()
|
||||
initNav()
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { imagesDivNodes as images } from './elemGen'
|
||||
import { imagesArrayLen } from './dataFetch'
|
||||
|
||||
export const renderImages = (): void => {
|
||||
images.forEach((img: HTMLImageElement, idx: number): void => {
|
||||
const randomX: number = Math.floor(Math.random() * 35) + 2
|
||||
let randomY: number
|
||||
|
||||
// random Y calculation
|
||||
if (idx === 0) {
|
||||
randomY = 68
|
||||
} else if (idx === 1) {
|
||||
randomY = 44
|
||||
} else if (idx === imagesArrayLen - 1) {
|
||||
randomY = 100
|
||||
} else {
|
||||
randomY = Math.floor(Math.random() * 51) + 2
|
||||
}
|
||||
|
||||
img.style.transform = `translate(${randomX}vw, -${randomY}%)`
|
||||
img.style.marginTop = `${idx === 1 ? 70 : 0}vh`
|
||||
img.style.visibility = 'visible'
|
||||
})
|
||||
}
|
||||
59
assets/ts/nav.ts
Normal file
59
assets/ts/nav.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { getState, incThreshold, decThreshold } from './state'
|
||||
import { expand } from './utils'
|
||||
|
||||
// threshold div
|
||||
const thresholdDiv = document
|
||||
.getElementsByClassName('threshold')
|
||||
.item(0) as HTMLDivElement
|
||||
|
||||
// threshold nums span
|
||||
const thresholdDispNums = Array.from(
|
||||
thresholdDiv.getElementsByClassName('num')
|
||||
) as HTMLSpanElement[]
|
||||
|
||||
// threshold buttons
|
||||
const decButton = thresholdDiv
|
||||
.getElementsByClassName('dec')
|
||||
.item(0) as HTMLButtonElement
|
||||
const incButton = thresholdDiv
|
||||
.getElementsByClassName('inc')
|
||||
.item(0) as HTMLButtonElement
|
||||
|
||||
// index div
|
||||
const indexDiv = document.getElementsByClassName('index').item(0) as HTMLDivElement
|
||||
|
||||
// index nums span
|
||||
const indexDispNums = Array.from(
|
||||
indexDiv.getElementsByClassName('num')
|
||||
) as HTMLSpanElement[]
|
||||
|
||||
export function initNav() {
|
||||
// init threshold text
|
||||
updateThresholdText()
|
||||
// init index text
|
||||
updateIndexText()
|
||||
// event listeners
|
||||
decButton.addEventListener('click', () => decThreshold())
|
||||
incButton.addEventListener('click', () => incThreshold())
|
||||
}
|
||||
|
||||
// helper
|
||||
|
||||
export function updateThresholdText(): void {
|
||||
const thresholdValue: string = expand(getState().threshold)
|
||||
thresholdDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
||||
e.innerText = thresholdValue[i]
|
||||
})
|
||||
}
|
||||
|
||||
export function updateIndexText(): void {
|
||||
const indexValue: string = expand(getState().index + 1)
|
||||
const indexLength: string = expand(getState().length)
|
||||
indexDispNums.forEach((e: HTMLSpanElement, i: number) => {
|
||||
if (i < 4) {
|
||||
e.innerText = indexValue[i]
|
||||
} else {
|
||||
e.innerText = indexLength[i - 4]
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import { delay, center, calcImageIndex, mouseToTransform, pushIndex } from './utils'
|
||||
import {
|
||||
handleOnMove,
|
||||
globalIndex,
|
||||
globalIndexDec,
|
||||
globalIndexInc,
|
||||
trailingImageIndexes,
|
||||
transformCache,
|
||||
emptyTransformCache,
|
||||
emptyTrailingImageIndexes,
|
||||
stackDepth,
|
||||
addEnterOverlayEL
|
||||
} from './desktop'
|
||||
import { imagesArrayLen } from './dataFetch'
|
||||
import { imgIndexSpanUpdate } from './indexDisp'
|
||||
import { overlayCursor, cursorInnerContent, imagesDivNodes as images } from './elemGen'
|
||||
|
||||
let oneThird: number = Math.round(window.innerWidth / 3)
|
||||
|
||||
// set cursor text
|
||||
const setCursorText = (text: string): void => {
|
||||
cursorInnerContent.innerText = text
|
||||
}
|
||||
|
||||
// overlay cursor event handler
|
||||
const setTextPos = (e: MouseEvent): void => {
|
||||
overlayCursor.style.transform = mouseToTransform(e.clientX, e.clientY, false, true)
|
||||
}
|
||||
|
||||
// disable listeners
|
||||
const disableListener = (): void => {
|
||||
window.removeEventListener('mousemove', handleOverlayMouseMove)
|
||||
overlayCursor.removeEventListener('click', handleOverlayClick)
|
||||
}
|
||||
|
||||
// enable overlay
|
||||
export const overlayEnable = (): void => {
|
||||
// show the overlay components
|
||||
overlayCursor.style.zIndex = '21'
|
||||
// set overlay event listeners
|
||||
setListener()
|
||||
}
|
||||
|
||||
// disable overlay
|
||||
export const overlayDisable = (): void => {
|
||||
// hide the overlay components
|
||||
overlayCursor.style.zIndex = '-1'
|
||||
// set overlay cursor text content to none
|
||||
setCursorText('')
|
||||
// disable overlay event listeners
|
||||
disableListener()
|
||||
}
|
||||
|
||||
// handle close click
|
||||
async function handleCloseClick(): Promise<void> {
|
||||
// disable overlay
|
||||
overlayDisable()
|
||||
// get length of indexes and empty indexes array
|
||||
const indexesNum = trailingImageIndexes.length
|
||||
emptyTrailingImageIndexes()
|
||||
// prepare animation
|
||||
for (let i: number = 0; i < indexesNum; i++) {
|
||||
// get element from index and store the index
|
||||
const index: number = calcImageIndex(globalIndex - i, imagesArrayLen)
|
||||
const e: HTMLImageElement = images[index]
|
||||
trailingImageIndexes.unshift(index)
|
||||
// set z index for the image element
|
||||
e.style.zIndex = `${indexesNum - i - 1}`
|
||||
// set different style for trailing and top image
|
||||
if (i === 0) {
|
||||
// set position
|
||||
e.style.transform = transformCache[indexesNum - i - 1]
|
||||
// set transition delay
|
||||
e.style.transitionDelay = '0s, 0.7s'
|
||||
// set status for css
|
||||
e.dataset.status = 'resumeTop'
|
||||
} else {
|
||||
// set position
|
||||
e.style.transform = transformCache[indexesNum - i - 1]
|
||||
// set transition delay
|
||||
e.style.transitionDelay = `${1.2 + 0.1 * i - 0.1}s`
|
||||
// set status for css
|
||||
e.dataset.status = 'resume'
|
||||
}
|
||||
// style process complete, show the image
|
||||
e.style.visibility = 'visible'
|
||||
}
|
||||
// halt the function while animation is running
|
||||
await delay(1200 + stackDepth * 100 + 100)
|
||||
// add back enter overlay event listener to top image
|
||||
addEnterOverlayEL(images[calcImageIndex(globalIndex, imagesArrayLen)])
|
||||
// clear unused status and transition delay
|
||||
for (let i: number = 0; i < indexesNum; i++) {
|
||||
const index: number = calcImageIndex(globalIndex - i, imagesArrayLen)
|
||||
images[index].dataset.status = 'null'
|
||||
images[index].style.transitionDelay = ''
|
||||
}
|
||||
// Add back previous self increment of global index (by handleOnMove)
|
||||
globalIndexInc()
|
||||
// add back mousemove event listener
|
||||
window.addEventListener('mousemove', handleOnMove, { passive: true })
|
||||
// empty the position array cache
|
||||
emptyTransformCache()
|
||||
}
|
||||
|
||||
const handleSideClick = (CLD: boolean): void => {
|
||||
// get last displayed image's index
|
||||
const imgIndex: number = calcImageIndex(globalIndex, imagesArrayLen)
|
||||
// change global index and get current displayed image's index
|
||||
CLD ? globalIndexInc() : globalIndexDec()
|
||||
const currImgIndex: number = calcImageIndex(globalIndex, imagesArrayLen)
|
||||
// store current displayed image's index
|
||||
CLD
|
||||
? pushIndex(
|
||||
currImgIndex,
|
||||
trailingImageIndexes,
|
||||
stackDepth,
|
||||
images,
|
||||
imagesArrayLen,
|
||||
false,
|
||||
false
|
||||
)
|
||||
: pushIndex(
|
||||
currImgIndex,
|
||||
trailingImageIndexes,
|
||||
stackDepth,
|
||||
images,
|
||||
imagesArrayLen,
|
||||
true,
|
||||
false
|
||||
)
|
||||
// hide last displayed image
|
||||
images[imgIndex].style.visibility = 'hidden'
|
||||
images[imgIndex].dataset.status = 'trail'
|
||||
// process the image going to display
|
||||
center(images[currImgIndex])
|
||||
images[currImgIndex].dataset.status = 'overlay'
|
||||
// process complete, show the image
|
||||
images[currImgIndex].style.visibility = 'visible'
|
||||
// change index display
|
||||
imgIndexSpanUpdate(currImgIndex + 1, imagesArrayLen)
|
||||
}
|
||||
|
||||
// change text and position of overlay cursor
|
||||
const handleOverlayMouseMove = (e: MouseEvent): void => {
|
||||
// set text position
|
||||
setTextPos(e)
|
||||
// set text content
|
||||
if (e.clientX < oneThird) {
|
||||
setCursorText('PREV')
|
||||
overlayCursor.dataset.status = 'PREV'
|
||||
} else if (e.clientX < oneThird * 2) {
|
||||
setCursorText('CLOSE')
|
||||
overlayCursor.dataset.status = 'CLOSE'
|
||||
} else {
|
||||
setCursorText('NEXT')
|
||||
overlayCursor.dataset.status = 'NEXT'
|
||||
}
|
||||
}
|
||||
|
||||
const handleOverlayClick = (): void => {
|
||||
switch (overlayCursor.dataset.status) {
|
||||
case 'PREV':
|
||||
handleSideClick(false)
|
||||
break
|
||||
case 'CLOSE':
|
||||
void handleCloseClick()
|
||||
break
|
||||
case 'NEXT':
|
||||
handleSideClick(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// set event listener
|
||||
const setListener = (): void => {
|
||||
// add mouse move event listener (for overlay text cursor)
|
||||
window.addEventListener('mousemove', handleOverlayMouseMove, { passive: true })
|
||||
// add close/prev/next click event listener
|
||||
overlayCursor.addEventListener('click', handleOverlayClick, { passive: true })
|
||||
}
|
||||
|
||||
export const vwRefreshInit = (): void => {
|
||||
window.addEventListener(
|
||||
'resize',
|
||||
() => {
|
||||
// refresh value of one third
|
||||
oneThird = Math.round(window.innerWidth / 3)
|
||||
// reset footer height
|
||||
const r = document.querySelector(':root') as HTMLStyleElement
|
||||
if (window.innerWidth > 768) {
|
||||
r.style.setProperty('--footer-height', '38px')
|
||||
} else {
|
||||
r.style.setProperty('--footer-height', '31px')
|
||||
}
|
||||
// recenter image (only in overlay)
|
||||
if (
|
||||
images[calcImageIndex(globalIndex, imagesArrayLen)].dataset.status === 'overlay'
|
||||
)
|
||||
center(images[calcImageIndex(globalIndex, imagesArrayLen)])
|
||||
},
|
||||
{ passive: true }
|
||||
)
|
||||
}
|
||||
21
assets/ts/resources.ts
Normal file
21
assets/ts/resources.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// data structure for images info
|
||||
export interface ImageJSON {
|
||||
index: number
|
||||
url: string
|
||||
imgH: number
|
||||
imgW: number
|
||||
pColor: string
|
||||
sColor: string
|
||||
}
|
||||
|
||||
export function initResources(): ImageJSON[] {
|
||||
const imagesJson = document.getElementById('imagesSource') as HTMLScriptElement
|
||||
return JSON.parse(imagesJson.textContent as string).sort(
|
||||
(a: ImageJSON, b: ImageJSON) => {
|
||||
if (a.index < b.index) {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
)
|
||||
}
|
||||
184
assets/ts/stage.ts
Normal file
184
assets/ts/stage.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { incIndex, getState } from './state'
|
||||
import { gsap, Power3 } from 'gsap'
|
||||
import { ImageJSON } from './resources'
|
||||
import { Watchable } from './utils'
|
||||
|
||||
// types
|
||||
|
||||
export type HistoryItem = { i: number; x: number; y: number }
|
||||
|
||||
// variables
|
||||
|
||||
let imgs: HTMLImageElement[] = []
|
||||
let last = { x: 0, y: 0 }
|
||||
export const cordHist = new Watchable<HistoryItem[]>([])
|
||||
export const isOpen = new Watchable<boolean>(false)
|
||||
export const isAnimating = new Watchable<boolean>(false)
|
||||
export const active = new Watchable<boolean>(false)
|
||||
|
||||
// getter
|
||||
|
||||
function getElTrail(): HTMLImageElement[] {
|
||||
return cordHist.get().map((item) => imgs[item.i])
|
||||
}
|
||||
|
||||
function getElTrailCurrent(): HTMLImageElement[] {
|
||||
return getElTrail().slice(-getState().trailLength)
|
||||
}
|
||||
|
||||
function getElTrailInactive(): HTMLImageElement[] {
|
||||
const elTrailCurrent = getElTrailCurrent()
|
||||
return elTrailCurrent.slice(0, elTrailCurrent.length - 1)
|
||||
}
|
||||
|
||||
function getElCurrent(): HTMLImageElement {
|
||||
const elTrail = getElTrail()
|
||||
return elTrail[elTrail.length - 1]
|
||||
}
|
||||
|
||||
// main functions
|
||||
|
||||
// on mouse
|
||||
function onMouse(e: MouseEvent): void {
|
||||
if (isOpen.get() || isAnimating.get()) return
|
||||
const cord = { x: e.clientX, y: e.clientY }
|
||||
const travelDist = Math.hypot(cord.x - last.x, cord.y - last.y)
|
||||
|
||||
if (travelDist > getState().threshold) {
|
||||
last = cord
|
||||
incIndex()
|
||||
|
||||
const newHist = { i: getState().index, ...cord }
|
||||
cordHist.set([...cordHist.get(), newHist].slice(-getState().length))
|
||||
}
|
||||
}
|
||||
|
||||
// set image position with gsap
|
||||
function setPositions(): void {
|
||||
const elTrail = getElTrail()
|
||||
if (!elTrail.length) return
|
||||
|
||||
gsap.set(elTrail, {
|
||||
x: (i: number) => cordHist.get()[i].x - window.innerWidth / 2,
|
||||
y: (i: number) => cordHist.get()[i].y - window.innerHeight / 2,
|
||||
opacity: (i: number) =>
|
||||
i + 1 + getState().trailLength <= cordHist.get().length ? 0 : 1,
|
||||
zIndex: (i: number) => i,
|
||||
scale: 0.6
|
||||
})
|
||||
|
||||
if (isOpen.get()) {
|
||||
gsap.set(imgs, { opacity: 0 })
|
||||
gsap.set(getElCurrent(), { opacity: 1, x: 0, y: 0, scale: 1 })
|
||||
}
|
||||
}
|
||||
|
||||
// open image into navigation
|
||||
function expandImage(): void {
|
||||
if (isAnimating.get()) return
|
||||
|
||||
isOpen.set(true)
|
||||
isAnimating.set(true)
|
||||
|
||||
const tl = gsap.timeline()
|
||||
// move down and hide trail inactive
|
||||
tl.to(getElTrailInactive(), {
|
||||
y: '+=20',
|
||||
ease: Power3.easeIn,
|
||||
stagger: 0.075,
|
||||
duration: 0.3,
|
||||
delay: 0.1,
|
||||
opacity: 0
|
||||
})
|
||||
// current move to center
|
||||
tl.to(getElCurrent(), {
|
||||
x: 0,
|
||||
y: 0,
|
||||
ease: Power3.easeInOut,
|
||||
duration: 0.7,
|
||||
delay: 0.3
|
||||
})
|
||||
// current expand
|
||||
tl.to(getElCurrent(), {
|
||||
delay: 0.1,
|
||||
scale: 1,
|
||||
ease: Power3.easeInOut
|
||||
})
|
||||
// finished
|
||||
tl.then(() => {
|
||||
isAnimating.set(false)
|
||||
})
|
||||
}
|
||||
|
||||
// close navigation and back to stage
|
||||
export function minimizeImage(): void {
|
||||
if (isAnimating.get()) return
|
||||
|
||||
isOpen.set(false)
|
||||
isAnimating.set(true)
|
||||
|
||||
const tl = gsap.timeline()
|
||||
// shrink current
|
||||
tl.to(getElCurrent(), {
|
||||
scale: 0.6,
|
||||
duration: 0.6,
|
||||
ease: Power3.easeInOut
|
||||
})
|
||||
// move current to original position
|
||||
tl.to(getElCurrent(), {
|
||||
delay: 0.3,
|
||||
duration: 0.7,
|
||||
ease: Power3.easeInOut,
|
||||
x: cordHist.get()[cordHist.get().length - 1].x - window.innerWidth / 2,
|
||||
y: cordHist.get()[cordHist.get().length - 1].y - window.innerHeight / 2
|
||||
})
|
||||
// show trail inactive
|
||||
tl.to(getElTrailInactive(), {
|
||||
y: '-=20',
|
||||
ease: Power3.easeOut,
|
||||
stagger: -0.1,
|
||||
duration: 0.3,
|
||||
opacity: 1
|
||||
})
|
||||
// finished
|
||||
tl.then(() => {
|
||||
isAnimating.set(false)
|
||||
})
|
||||
}
|
||||
|
||||
// init
|
||||
|
||||
export function initStage(ijs: ImageJSON[]): void {
|
||||
// create stage element
|
||||
createStage(ijs)
|
||||
// get stage
|
||||
const stage = document.getElementsByClassName('stage').item(0) as HTMLDivElement
|
||||
// get image elements
|
||||
imgs = Array.from(stage.getElementsByTagName('img'))
|
||||
// event listeners
|
||||
stage.addEventListener('click', () => expandImage())
|
||||
stage.addEventListener('keydown', () => expandImage())
|
||||
window.addEventListener('mousemove', onMouse)
|
||||
// watchers
|
||||
isOpen.addWatcher(() => active.set(isOpen.get() && !isAnimating.get()))
|
||||
isAnimating.addWatcher(() => active.set(isOpen.get() && !isAnimating.get()))
|
||||
cordHist.addWatcher(() => setPositions())
|
||||
}
|
||||
|
||||
// hepler
|
||||
|
||||
function createStage(ijs: ImageJSON[]): void {
|
||||
// create container for images
|
||||
const stage: HTMLDivElement = document.createElement('div')
|
||||
stage.className = 'stage'
|
||||
// append images to container
|
||||
for (let ij of ijs) {
|
||||
const e = document.createElement('img')
|
||||
e.src = ij.url
|
||||
e.height = ij.imgH
|
||||
e.width = ij.imgW
|
||||
e.alt = 'image'
|
||||
stage.append(e)
|
||||
}
|
||||
document.getElementById('main')!.append(stage)
|
||||
}
|
||||
87
assets/ts/stageNav.ts
Normal file
87
assets/ts/stageNav.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { setCustomCursor } from './customCursor'
|
||||
import { decIndex, incIndex, getState } from './state'
|
||||
import { increment, decrement } from './utils'
|
||||
import { cordHist, isOpen, isAnimating, active, minimizeImage } from './stage'
|
||||
|
||||
type NavItem = (typeof navItems)[number]
|
||||
const navItems = ['prev', 'close', 'next'] as const
|
||||
|
||||
// main functions
|
||||
|
||||
function handleClick(type: NavItem) {
|
||||
switch (type) {
|
||||
case 'prev':
|
||||
prevImage()
|
||||
break
|
||||
case 'close':
|
||||
minimizeImage()
|
||||
break
|
||||
case 'next':
|
||||
nextImage()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
if (isOpen.get() || isAnimating.get()) return
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
prevImage()
|
||||
break
|
||||
case 'Escape':
|
||||
minimizeImage()
|
||||
break
|
||||
case 'ArrowRight':
|
||||
nextImage()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// init
|
||||
|
||||
export function initStageNav() {
|
||||
const navOverlay = document.createElement('div')
|
||||
navOverlay.className = 'navOverlay'
|
||||
for (let navItem of navItems) {
|
||||
const overlay = document.createElement('div')
|
||||
overlay.className = 'overlay'
|
||||
overlay.addEventListener('click', () => handleClick(navItem))
|
||||
overlay.addEventListener('keydown', () => handleClick(navItem))
|
||||
overlay.addEventListener('mouseover', () => setCustomCursor(navItem))
|
||||
overlay.addEventListener('focus', () => setCustomCursor(navItem))
|
||||
navOverlay.append(overlay)
|
||||
}
|
||||
active.addWatcher(() => {
|
||||
if (active.get()) {
|
||||
navOverlay.classList.add('active')
|
||||
} else {
|
||||
navOverlay.classList.remove('active')
|
||||
}
|
||||
})
|
||||
document.getElementById('main')!.append(navOverlay)
|
||||
window.addEventListener('keydown', handleKey)
|
||||
}
|
||||
|
||||
// hepler
|
||||
|
||||
function nextImage() {
|
||||
if (isAnimating.get()) return
|
||||
cordHist.set(
|
||||
cordHist.get().map((item) => {
|
||||
return { ...item, i: increment(item.i, getState().length) }
|
||||
})
|
||||
)
|
||||
|
||||
incIndex()
|
||||
}
|
||||
|
||||
function prevImage() {
|
||||
if (isAnimating.get()) return
|
||||
cordHist.set(
|
||||
cordHist.get().map((item) => {
|
||||
return { ...item, i: decrement(item.i, getState().length) }
|
||||
})
|
||||
)
|
||||
|
||||
decIndex()
|
||||
}
|
||||
68
assets/ts/state.ts
Normal file
68
assets/ts/state.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { increment, decrement } from './utils'
|
||||
import { updateIndexText, updateThresholdText } from './nav'
|
||||
|
||||
const thresholds = [
|
||||
{ threshold: 20, trailLength: 20 },
|
||||
{ threshold: 40, trailLength: 10 },
|
||||
{ threshold: 80, trailLength: 5 },
|
||||
{ threshold: 140, trailLength: 5 },
|
||||
{ threshold: 200, trailLength: 5 }
|
||||
]
|
||||
|
||||
const defaultState = {
|
||||
index: -1,
|
||||
length: 0,
|
||||
threshold: thresholds[2].threshold,
|
||||
trailLength: thresholds[2].trailLength
|
||||
}
|
||||
|
||||
export type State = typeof defaultState
|
||||
|
||||
let state = defaultState
|
||||
|
||||
export function getState(): State {
|
||||
// return a copy of state
|
||||
return Object.create(
|
||||
Object.getPrototypeOf(state),
|
||||
Object.getOwnPropertyDescriptors(state)
|
||||
)
|
||||
}
|
||||
|
||||
export function initState(length: number): void {
|
||||
state.length = length
|
||||
}
|
||||
|
||||
export function setIndex(index: number): void {
|
||||
state.index = index
|
||||
updateIndexText()
|
||||
}
|
||||
|
||||
export function incIndex(): void {
|
||||
state.index = increment(state.index, state.length)
|
||||
updateIndexText()
|
||||
}
|
||||
|
||||
export function decIndex(): void {
|
||||
state.index = decrement(state.index, state.length)
|
||||
updateIndexText()
|
||||
}
|
||||
|
||||
export function incThreshold(): void {
|
||||
state = updateThreshold(state, 1)
|
||||
updateThresholdText()
|
||||
}
|
||||
|
||||
export function decThreshold(): void {
|
||||
state = updateThreshold(state, -1)
|
||||
updateThresholdText()
|
||||
}
|
||||
|
||||
// helper
|
||||
|
||||
function updateThreshold(state: State, inc: number): State {
|
||||
const i = thresholds.findIndex((t) => state.threshold === t.threshold)
|
||||
const newItems = thresholds[i + inc]
|
||||
// out of range
|
||||
if (!newItems) return state
|
||||
return { ...state, ...newItems }
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { duper } from './utils'
|
||||
import { setStackDepth } from './desktop'
|
||||
|
||||
// get threshold display element
|
||||
const thresholdDisp = document.getElementsByClassName('thid').item(0) as HTMLSpanElement
|
||||
|
||||
// threshold data
|
||||
const threshold: number[] = [0, 40, 80, 120, 160, 200]
|
||||
export const thresholdSensitivityArray: number[] = [100, 40, 18, 14, 9, 5]
|
||||
export let thresholdIndex: number = 2
|
||||
|
||||
export const stackDepthArray: number[] = [15, 8, 5, 5, 5, 5]
|
||||
|
||||
// update inner text of threshold display element
|
||||
const thresholdUpdate = (): void => {
|
||||
thresholdDisp.innerText = duper(threshold[thresholdIndex])
|
||||
}
|
||||
|
||||
const stackDepthUpdate = (): void => {
|
||||
setStackDepth(stackDepthArray[thresholdIndex])
|
||||
}
|
||||
|
||||
// threshold control initialization
|
||||
export const thresholdCtlInit = (): void => {
|
||||
thresholdUpdate()
|
||||
const dec = document.getElementById('thresholdDec') as HTMLButtonElement
|
||||
dec.addEventListener(
|
||||
'click',
|
||||
function () {
|
||||
if (thresholdIndex > 0) {
|
||||
thresholdIndex--
|
||||
thresholdUpdate()
|
||||
stackDepthUpdate()
|
||||
}
|
||||
},
|
||||
{ passive: true }
|
||||
)
|
||||
|
||||
const inc = document.getElementById('thresholdInc') as HTMLButtonElement
|
||||
inc.addEventListener(
|
||||
'click',
|
||||
function () {
|
||||
if (thresholdIndex < 5) {
|
||||
thresholdIndex++
|
||||
thresholdUpdate()
|
||||
stackDepthUpdate()
|
||||
}
|
||||
},
|
||||
{ passive: true }
|
||||
)
|
||||
}
|
||||
@@ -1,145 +1,33 @@
|
||||
export interface ImageData {
|
||||
index: string
|
||||
url: string
|
||||
imgH: string
|
||||
imgW: string
|
||||
pColor: string
|
||||
sColor: string
|
||||
export function increment(num: number, length: number): number {
|
||||
return (num + 1) % length
|
||||
}
|
||||
|
||||
export interface position {
|
||||
x: number
|
||||
y: number
|
||||
export function decrement(num: number, length: number): number {
|
||||
return (num + length - 1) % length
|
||||
}
|
||||
|
||||
export interface deviceType {
|
||||
mobile: boolean
|
||||
tablet: boolean
|
||||
desktop: boolean
|
||||
}
|
||||
|
||||
// 0 to 0001, 25 to 0025
|
||||
export const duper = (num: number): string => {
|
||||
export function expand(num: number): string {
|
||||
return ('0000' + num.toString()).slice(-4)
|
||||
}
|
||||
|
||||
export const mouseToTransform = (
|
||||
x: number,
|
||||
y: number,
|
||||
centerCorrection: boolean = true,
|
||||
accelerate: boolean = false
|
||||
): string => {
|
||||
return `translate${accelerate ? '3d' : ''}(${
|
||||
centerCorrection ? `calc(${x}px - 50%)` : `${x}px`
|
||||
}, ${centerCorrection ? `calc(${y}px - 50%)` : `${y}px`}${accelerate ? ', 0' : ''})`
|
||||
export function isMobile(): boolean {
|
||||
return window.matchMedia('(hover: none)').matches
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
export function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
export class Watchable<T> {
|
||||
constructor(private obj: T) {}
|
||||
private watchers: (() => void)[] = []
|
||||
|
||||
// remove all event listeners from a node
|
||||
export const removeAllEventListeners = (e: Node): Node => {
|
||||
return e.cloneNode(true)
|
||||
}
|
||||
|
||||
// center top div
|
||||
export const center = (e: HTMLElement): void => {
|
||||
const x: number = window.innerWidth / 2
|
||||
let y: number
|
||||
if (window.innerWidth > 768) {
|
||||
y = (window.innerHeight - 38) / 2
|
||||
} else {
|
||||
y = (window.innerHeight - 31) / 2 + 31
|
||||
get(): T {
|
||||
return this.obj
|
||||
}
|
||||
e.style.transform = mouseToTransform(x, y)
|
||||
}
|
||||
|
||||
export const createImgElement = (input: ImageData): HTMLImageElement => {
|
||||
const img = document.createElement('img')
|
||||
img.setAttribute('src', input.url)
|
||||
img.setAttribute('alt', '')
|
||||
img.setAttribute('height', input.imgH)
|
||||
img.setAttribute('width', input.imgW)
|
||||
img.style.visibility = 'hidden'
|
||||
img.dataset.status = 'trail'
|
||||
// img.style.backgroundImage = `linear-gradient(15deg, ${input.pColor}, ${input.sColor})`
|
||||
return img
|
||||
}
|
||||
set(e: T): void {
|
||||
this.obj = e
|
||||
this.watchers.forEach((watcher) => watcher())
|
||||
}
|
||||
|
||||
export const calcImageIndex = (index: number, imgCounts: number): number => {
|
||||
if (index >= 0) {
|
||||
return index % imgCounts
|
||||
} else {
|
||||
return (imgCounts + (index % imgCounts)) % imgCounts
|
||||
addWatcher(watcher: () => void): void {
|
||||
this.watchers.push(watcher)
|
||||
}
|
||||
}
|
||||
|
||||
export const preloadImage = (src: string): void => {
|
||||
const cache = new Image()
|
||||
cache.src = src
|
||||
}
|
||||
|
||||
export const getDeviceType = (): deviceType => {
|
||||
const ua: string = navigator.userAgent
|
||||
const result: deviceType = { mobile: false, tablet: false, desktop: false }
|
||||
if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) {
|
||||
result.mobile = true
|
||||
result.tablet = true
|
||||
} else if (
|
||||
/Mobile|iP(hone|od)|Android|BlackBerry|IEMobile|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test(
|
||||
ua
|
||||
)
|
||||
) {
|
||||
result.mobile = true
|
||||
} else result.desktop = true
|
||||
return result
|
||||
}
|
||||
|
||||
export const hideImage = (e: HTMLImageElement): void => {
|
||||
e.style.visibility = 'hidden'
|
||||
e.dataset.status = 'trail'
|
||||
}
|
||||
|
||||
export const pushIndex = (
|
||||
index: number,
|
||||
indexesArray: number[],
|
||||
stackDepth: number,
|
||||
imagesArray: NodeListOf<HTMLImageElement>,
|
||||
imagesArrayLen: number,
|
||||
invertFlag: boolean = false,
|
||||
autoHideFlag: boolean = true
|
||||
): number => {
|
||||
let indexesNum: number = indexesArray.length
|
||||
// create variable overflow to store the tail index
|
||||
let overflow: number
|
||||
if (!invertFlag) {
|
||||
// if stack is full, push the tail index out and hide the image
|
||||
if (indexesNum === stackDepth) {
|
||||
// insert
|
||||
indexesArray.push(index)
|
||||
// pop out
|
||||
overflow = indexesArray.shift() as number
|
||||
// auto hide tail image
|
||||
if (autoHideFlag) hideImage(imagesArray[overflow])
|
||||
} else {
|
||||
indexesArray.push(index)
|
||||
indexesNum += 1
|
||||
}
|
||||
} else {
|
||||
// if stack is full, push the tail index out and hide the image
|
||||
if (indexesNum === stackDepth) {
|
||||
// insert
|
||||
indexesArray.unshift(calcImageIndex(index - stackDepth + 1, imagesArrayLen))
|
||||
// pop out
|
||||
overflow = indexesArray.pop() as number
|
||||
// auto hide tail image
|
||||
if (autoHideFlag) hideImage(imagesArray[overflow])
|
||||
} else {
|
||||
indexesArray.unshift(calcImageIndex(index - indexesNum + 1, imagesArrayLen))
|
||||
indexesNum += 1
|
||||
}
|
||||
}
|
||||
return indexesNum
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user