# <type><package>: <subject> {needed}
# 
# <body> {optional}
# 
# <footer> {optional}
# 
# example:
# feat(login): implementation login api function
#
# finished login module and integration with server login api
# 
# <Type>
# feat: new feature
# fix: bug fix
# docs: docs only changes
# style: style changes
# refactor: feature refactor
# perf: performance optimize
# test: test related changes
# build: build related changes
# ci: ci related changes
# chore: changes not related to src or test files
# revert: reverts a previous commit
# 
# <Subject>
# describe all major changes briefly
# 
# <Body>
# detailed info on major changes
#
This commit is contained in:
Sped0n
2023-10-29 12:50:14 +08:00
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.2.2"
},
"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"
}
}