PageSourceSearch

https://www.mehai.dev/workshop-bento.js

js mehai.dev collected 2026-10-03 23:20:22 UTC 9,373 bytes, 263 lines download raw bytes

1// Workshop Bento Box Animation - Exact copy from brainbodyfm-workshop
2class WorkshopBentoAnimation {
3    constructor() {
4        this.container = document.querySelector('.workshop-biosignal-background');
5        if (!this.container) return;
6
7        this.rows = [];
8        this.animationId = null;
9        this.isRunning = false;
10
11        // Configuration - exact same as brainbodyfm-workshop
12        this.config = {
13            rowCount: 5, // Same as original
14            iconSpacing: 140, // Same as original
15            speed: 0.15, // Slow speed for bento box
16            iconSize: 100, // Same as original
17            rowHeight: 140, // Same as original
18            rowSpacing: 22, // Same as original
19            rotation: 8, // Same as original
20            icons: [
21                'assets/modalities/eeg.png',
22                'assets/modalities/fmri.png',
23                'assets/modalities/emg.png',
24                'assets/modalities/meg.png',
25                'assets/modalities/ecg.png',
26                'assets/modalities/ieeg.png',
27                'assets/modalities/wearable.png',
28                'assets/modalities/calcium.png',
29                'assets/modalities/microarray.png',
30                'assets/modalities/eda.png',
31                'assets/modalities/eog.png',
32                'assets/modalities/ppg.png',
33                'assets/modalities/psg.png',
34                'assets/modalities/seeg.png',
35                'assets/modalities/monkey.png',
36                'assets/modalities/c-elegans.png',
37            ]
38        };
39
40        // Create shuffled icon sequences for each row
41        this.iconSequences = [];
42        this.init();
43    }
44
45    // Helper function to shuffle array
46    shuffleArray(array) {
47        const shuffled = [...array];
48        for (let i = shuffled.length - 1; i > 0; i--) {
49            const j = Math.floor(Math.random() * (i + 1));
50            [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
51        }
52        return shuffled;
53    }
54
55    // Create a randomized but distributed sequence of icons
56    createIconSequence(length) {
57        const sequence = [];
58        const iconCount = this.config.icons.length;
59
60        // Create multiple shuffled copies of the icon array
61        const copies = Math.ceil(length / iconCount);
62        for (let i = 0; i < copies; i++) {
63            sequence.push(...this.shuffleArray(this.config.icons));
64        }
65
66        // Trim to exact length needed
67        return sequence.slice(0, length);
68    }
69
70    init() {
71        // Clear existing content
72        this.container.innerHTML = '';
73
74        // Create rows
75        for (let i = 0; i < this.config.rowCount; i++) {
76            this.createRow(i);
77        }
78
79        this.start();
80    }
81
82    createRow(index) {
83        const row = document.createElement('div');
84        row.className = 'workshop-biosignal-row-js';
85        row.style.cssText = `
86            position: absolute;
87            width: 100%;
88            height: ${this.config.rowHeight}px;
89            display: flex;
90            align-items: center;
91            top: ${-25 + (index * this.config.rowSpacing)}%;
92            transform: rotate(${this.config.rotation}deg);
93            pointer-events: none;
94        `;
95
96        // Calculate how many icons we need to fill the screen plus buffer
97        const screenWidth = window.innerWidth;
98        const screenHeight = window.innerHeight;
99        const diagonal = Math.sqrt(screenWidth * screenWidth + screenHeight * screenHeight);
100        const rotationBuffer = diagonal * 0.6;
101        const totalWidth = diagonal + rotationBuffer;
102        const iconsNeeded = Math.ceil(totalWidth / this.config.iconSpacing) + 10;
103
104        // Create a randomized icon sequence for this row
105        const iconSequence = this.createIconSequence(iconsNeeded);
106        this.iconSequences[index] = iconSequence;
107
108        // Create row data with consistent speed
109        const rowData = {
110            element: row,
111            icons: [],
112            speed: this.config.speed,
113            offset: (index % 2) * (this.config.iconSpacing / 2),
114            iconSequence: iconSequence
115        };
116
117        // Create icons for this row using the randomized sequence
118        for (let i = 0; i < iconsNeeded; i++) {
119            const iconData = this.createIcon(i, rowData.offset, iconSequence[i]);
120            row.appendChild(iconData.element);
121            rowData.icons.push(iconData);
122        }
123
124        this.container.appendChild(row);
125        this.rows.push(rowData);
126    }
127
128    createIcon(index, baseOffset, iconSrc) {
129        const img = document.createElement('img');
130
131        img.src = iconSrc;
132        img.className = 'workshop-biosignal-icon-js';
133        img.alt = '';
134        img.style.cssText = `
135            width: ${this.config.iconSize}px;
136            height: ${this.config.iconSize}px;
137            object-fit: contain;
138            opacity: 0.9;
139            flex-shrink: 0;
140            position: absolute;
141            animation: none;
142        `;
143
144        // Start icons off-screen to the left with proper spacing
145        const startX = -this.config.iconSpacing * 4 + baseOffset + (index * this.config.iconSpacing);
146
147        return {
148            element: img,
149            currentX: startX,
150            originalIndex: index,
151            floatOffset: Math.random() * Math.PI * 2,
152            floatSpeed: 0.015 + Math.random() * 0.005
153        };
154    }
155
156    updatePositions() {
157        const time = Date.now() * 0.001;
158        const screenWidth = window.innerWidth;
159        const screenHeight = window.innerHeight;
160
161        const diagonal = Math.sqrt(screenWidth * screenWidth + screenHeight * screenHeight);
162        const resetPoint = diagonal * 1.0;
163        const startPoint = -this.config.iconSpacing * 5;
164
165        this.rows.forEach((row, rowIndex) => {
166            row.icons.forEach((icon, iconIndex) => {
167                // Update horizontal position with consistent speed
168                icon.currentX += row.speed;
169
170                // Reset position when icon goes far enough off screen
171                if (icon.currentX > resetPoint) {
172                    const leftmostX = Math.min(...row.icons.map(i => i.currentX));
173                    icon.currentX = leftmostX - this.config.iconSpacing;
174
175                    // When resetting, assign a new random icon from the sequence
176                    const newIconIndex = (iconIndex + row.icons.length) % row.iconSequence.length;
177                    icon.element.src = row.iconSequence[newIconIndex];
178                }
179
180                // Calculate subtle floating animation
181                const floatY = Math.sin(time * icon.floatSpeed + icon.floatOffset) * 6;
182                const floatScale = 1 + Math.sin(time * icon.floatSpeed + icon.floatOffset) * 0.03;
183
184                // Apply transform
185                icon.element.style.transform = `
186                    translateX(${icon.currentX}px) 
187                    translateY(${floatY}px) 
188                    scale(${floatScale})
189                `;
190            });
191        });
192    }
193
194    start() {
195        if (this.isRunning) return;
196        this.isRunning = true;
197
198        const animate = () => {
199            if (!this.isRunning) return;
200
201            this.updatePositions();
202            this.animationId = requestAnimationFrame(animate);
203        };
204
205        animate();
206    }
207
208    stop() {
209        this.isRunning = false;
210        if (this.animationId) {
211            cancelAnimationFrame(this.animationId);
212            this.animationId = null;
213        }
214    }
215
216    handleResize() {
217        const screenWidth = window.innerWidth;
218        const screenHeight = window.innerHeight;
219        const diagonal = Math.sqrt(screenWidth * screenWidth + screenHeight * screenHeight);
220        const rotationBuffer = diagonal * 0.6;
221        const totalWidth = diagonal + rotationBuffer;
222        const iconsNeeded = Math.ceil(totalWidth / this.config.iconSpacing) + 10;
223
224        this.rows.forEach((row, rowIndex) => {
225            // Add more icons if we don't have enough for the new screen size
226            while (row.icons.length < iconsNeeded) {
227                // Extend the icon sequence if needed
228                if (row.iconSequence.length <= row.icons.length) {
229                    const additionalIcons = this.createIconSequence(iconsNeeded - row.iconSequence.length);
230                    row.iconSequence.push(...additionalIcons);
231                }
232
233                const iconSrc = row.iconSequence[row.icons.length];
234                const iconData = this.createIcon(row.icons.length, row.offset, iconSrc);
235                row.element.appendChild(iconData.element);
236                row.icons.push(iconData);
237            }
238        });
239    }
240}
241
242// Initialize when DOM is loaded
243document.addEventListener('DOMContentLoaded', () => {
244    const workshopBentoAnimation = new WorkshopBentoAnimation();
245
246    // Handle window resize
247    let resizeTimeout;
248    window.addEventListener('resize', () => {
249        clearTimeout(resizeTimeout);
250        resizeTimeout = setTimeout(() => {
251            workshopBentoAnimation.handleResize();
252        }, 250);
253    });
254
255    // Pause animation when page is not visible (performance optimization)
256    document.addEventListener('visibilitychange', () => {
257        if (document.hidden) {
258            workshopBentoAnimation.stop();
259        } else {
260            workshopBentoAnimation.start();
261        }
262    });
263});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.