* 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:
Spedon
2023-10-29 12:39:56 +08:00
committed by GitHub
parent 15eafc7ea0
commit 4387abe52f
50 changed files with 2963 additions and 1100 deletions

5
.idea/.gitignore generated vendored
View File

@@ -1,5 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

12
.idea/bridget.iml generated
View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,93 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="HTML_QUOTE_STYLE" value="Single" />
<option name="HTML_ENFORCE_QUOTES" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="CSS">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="88" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="88" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="LESS">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="SASS">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="SCSS">
<indentOptions>
<option name="TAB_SIZE" value="2" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="88" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="88" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<custom-configuration-file used="true" path="$PROJECT_DIR$/.eslintrc.json" />
</component>
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/bridget.iml" filepath="$PROJECT_DIR$/.idea/bridget.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions" suppressed-tasks="SCSS" />
</project>

View File

@@ -1,9 +1,18 @@
{
"useTabs": false,
"tabWidth": 2,
"printWidth": 88,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"semi": false
"useTabs": false,
"tabWidth": 2,
"printWidth": 88,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"semi": false,
"plugins": ["prettier-plugin-go-template"],
"overrides": [
{
"files": ["*.html"],
"options": {
"parser": "go-template"
}
}
]
}

View File

@@ -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;
}

View File

@@ -0,0 +1,6 @@
@font-face {
font-family: HelveticaNow;
src: url('/fonts/HelveticaNowText-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
}

View 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)}.";
}
}

View 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;
}

View 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;
}
}

View 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);
}

View File

@@ -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;
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -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);
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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;
}

View File

@@ -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
View 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
}

View File

@@ -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

View File

@@ -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])
}
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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]
}
}
}

View File

@@ -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()

View File

@@ -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
View 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]
}
})
}

View File

@@ -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
View 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
View 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
View 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
View 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 }
}

View File

@@ -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 }
)
}

View File

@@ -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
}

View File

@@ -1,43 +1,42 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
{{ partial "head.html" . }}
<title>{{ .Title }}</title>
</head>
<body>
<header>
{{ partial "header.html" . }}
</header>
<div id="main">
{{ $sourcePath := "images" }}
{{ $gallery := site.GetPage $sourcePath }}
{{ with $gallery.Resources.ByType "image" }}
{{ $index := len . }}
{{ $.Scratch.Add "img" slice }}
{{ range . }}
{{ $index = sub $index 1}}
{{ $colors := .Colors }}
{{ $pColor := index $colors 0 }}
{{ $sColor := "#ccc" }}
{{ if gt (len $colors) 1 }}
{{ $sColor = index $colors 1 }}
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{{ partial "head.html" . }}
<title>{{ .Title }}</title>
</head>
<body>
<header>
{{ partial "header.html" . }}
</header>
<div id="main">
{{ $sourcePath := "images" }}
{{ $gallery := site.GetPage $sourcePath }}
{{ with $gallery.Resources.ByType "image" }}
{{ $index := len . }}
{{ $.Scratch.Add "img" slice }}
{{ range . }}
{{ $index = sub $index 1 }}
{{ $colors := .Colors }}
{{ $pColor := index $colors 0 }}
{{ $sColor := "#ccc" }}
{{ if gt (len $colors) 1 }}
{{ $sColor = index $colors 1 }}
{{ end }}
{{ $resize := .Resize "x2000 webp Lanczos q70" }}
{{ $.Scratch.Add "img" (dict
"index" (int $index)
"url" (string .RelPermalink)
"imgH" (int .Height)
"imgW" (int .Width)
"pColor" (string $pColor)
"sColor" (string $sColor))
}}
{{ end }}
<script id="imagesSource" type="application/json">{{ $.Scratch.Get "img" | jsonify | safeJS }}</script>
{{ end }}
{{ $resize := .Resize "x2000 webp Lanczos q70" }}
{{ $.Scratch.Add "img" (dict
"index" (string $index)
"url" (string .RelPermalink)
"imgH" (string .Height)
"imgW" (string .Width)
"pColor" (string $pColor)
"sColor" (string $sColor)) }}
{{ end }}
<script id="images_array" type="application/json">{{ $.Scratch.Get "img" | jsonify | safeJS }}</script>
{{ end }}
</div>
<footer>
{{ partial "footer.html" . }}
</footer>
</body>
</div>
{{ partial "nav.html" . }}
</body>
</html>

View File

@@ -3,6 +3,7 @@
{{- $options := dict "targetPath" "css/style.min.css" "enableSourceMap" true -}}
{{- $style = dict "Context" . "ToCSS" $options | merge $style -}}
{{- partial "plugin/style.html" $style -}}
{{- $esBuildOpts := dict "minify" hugo.IsProduction -}}
{{ $script := resources.Get "ts/main.ts" | js.Build }}
<script type="text/javascript" src="{{ $script.RelPermalink }}" defer></script>
{{- $script := resources.Get "ts/main.ts" | js.Build $esBuildOpts -}}
<script type="text/javascript" src="{{ $script.RelPermalink }}" defer></script>

27
layouts/partials/nav.html Normal file
View File

@@ -0,0 +1,27 @@
<nav>
<div class="navArtist">
<a href="/">Bridget Baker</a>
</div>
<div class="links">
<span class="link">Featured</span>
<span class="link">iPhone</span>
<span class="link">Film</span>
<span class="link">Info</span>
</div>
<div class="threshold">
<span>Threshold:</span>
<span>
<button class="dec"></button>
<span class="num"></span><span class="num"></span><span class="num"></span
><span class="num"></span>
<button class="inc"></button>
</span>
</div>
<div class="index">
<span class="num"></span><span class="num"></span><span class="num"></span
><span class="num"></span>
<span>/</span>
<span class="num"></span><span class="num"></span><span class="num"></span
><span class="num"></span>
</div>
</nav>

View File

@@ -37,6 +37,10 @@
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-promise": "^6.1.1",
"prettier": "3.0.3",
"prettier-plugin-go-template": "^0.0.15",
"typescript": "^5.1.6"
},
"dependencies": {
"gsap": "^3.12.2"
}
}

2088
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -8,8 +8,9 @@
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
},
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Recommended"
}
}