Fixing share link
This commit is contained in:
@ -53,8 +53,12 @@ type ShaderFunction = (...args: number[]) => number;
|
||||
class ShaderWorker {
|
||||
private compiledFunction: ShaderFunction | null = null;
|
||||
private lastCode: string = '';
|
||||
private imageDataCache: LRUCache<string, ImageData> = new LRUCache(PERFORMANCE.IMAGE_DATA_CACHE_SIZE);
|
||||
private compilationCache: LRUCache<string, ShaderFunction> = new LRUCache(PERFORMANCE.COMPILATION_CACHE_SIZE);
|
||||
private imageDataCache: LRUCache<string, ImageData> = new LRUCache(
|
||||
PERFORMANCE.IMAGE_DATA_CACHE_SIZE
|
||||
);
|
||||
private compilationCache: LRUCache<string, ShaderFunction> = new LRUCache(
|
||||
PERFORMANCE.COMPILATION_CACHE_SIZE
|
||||
);
|
||||
private colorTables: Map<string, Uint8Array> = new Map();
|
||||
private feedbackBuffer: Float32Array | null = null;
|
||||
|
||||
@ -66,7 +70,6 @@ class ShaderWorker {
|
||||
this.initializeColorTables();
|
||||
}
|
||||
|
||||
|
||||
private initializeColorTables(): void {
|
||||
const tableSize = COLOR_TABLE_SIZE;
|
||||
|
||||
@ -96,7 +99,6 @@ class ShaderWorker {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private handleMessage(message: WorkerMessage): void {
|
||||
try {
|
||||
switch (message.type) {
|
||||
@ -412,14 +414,19 @@ class ShaderWorker {
|
||||
const v = actualY / fullHeight;
|
||||
const centerX = fullWidth / 2;
|
||||
const centerY = fullHeight / 2;
|
||||
const radius = Math.sqrt((x - centerX) ** 2 + (actualY - centerY) ** 2);
|
||||
const radius = Math.sqrt(
|
||||
(x - centerX) ** 2 + (actualY - centerY) ** 2
|
||||
);
|
||||
const angle = Math.atan2(actualY - centerY, x - centerX);
|
||||
const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2);
|
||||
const normalizedDistance = radius / maxDistance;
|
||||
const frameCount = Math.floor(time * 60);
|
||||
const manhattanDistance = Math.abs(x - centerX) + Math.abs(actualY - centerY);
|
||||
const manhattanDistance =
|
||||
Math.abs(x - centerX) + Math.abs(actualY - centerY);
|
||||
const noise = (Math.sin(x * 0.1) * Math.cos(actualY * 0.1) + 1) * 0.5;
|
||||
const feedbackValue = this.feedbackBuffer ? this.feedbackBuffer[pixelIndex] || 0 : 0;
|
||||
const feedbackValue = this.feedbackBuffer
|
||||
? this.feedbackBuffer[pixelIndex] || 0
|
||||
: 0;
|
||||
|
||||
const value = this.compiledFunction!(
|
||||
x,
|
||||
@ -515,7 +522,6 @@ class ShaderWorker {
|
||||
if (!imageData) {
|
||||
imageData = new ImageData(width, height);
|
||||
this.imageDataCache.set(key, imageData);
|
||||
|
||||
}
|
||||
|
||||
return imageData;
|
||||
@ -623,13 +629,14 @@ class ShaderWorker {
|
||||
|
||||
for (let i = 0; i < octaves; i++) {
|
||||
const frequency = Math.pow(2, i) * scale;
|
||||
const noise = Math.sin((x + Math.abs(value) * 0.1) * frequency) *
|
||||
const noise =
|
||||
Math.sin((x + Math.abs(value) * 0.1) * frequency) *
|
||||
Math.cos((y + Math.abs(value) * 0.1) * frequency);
|
||||
fractalValue += noise * amplitude;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
|
||||
processedValue = Math.floor(((fractalValue + 1) * 0.5) * 255);
|
||||
processedValue = Math.floor((fractalValue + 1) * 0.5 * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -638,20 +645,24 @@ class ShaderWorker {
|
||||
const cellSize = 16;
|
||||
const cellX = Math.floor(x / cellSize);
|
||||
const cellY = Math.floor(y / cellSize);
|
||||
const cellHash = (cellX * 73856093) ^ (cellY * 19349663) ^ Math.floor(Math.abs(value));
|
||||
const cellHash =
|
||||
(cellX * 73856093) ^ (cellY * 19349663) ^ Math.floor(Math.abs(value));
|
||||
|
||||
// Generate cellular pattern based on neighbors
|
||||
let neighbors = 0;
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
if (dx === 0 && dy === 0) continue;
|
||||
const neighborHash = ((cellX + dx) * 73856093) ^ ((cellY + dy) * 19349663) ^ Math.floor(Math.abs(value));
|
||||
if ((neighborHash % 256) > 128) neighbors++;
|
||||
const neighborHash =
|
||||
((cellX + dx) * 73856093) ^
|
||||
((cellY + dy) * 19349663) ^
|
||||
Math.floor(Math.abs(value));
|
||||
if (neighborHash % 256 > 128) neighbors++;
|
||||
}
|
||||
}
|
||||
|
||||
const cellState = (cellHash % 256) > 128 ? 1 : 0;
|
||||
const evolution = (neighbors >= 3 && neighbors <= 5) ? 1 : cellState;
|
||||
const cellState = cellHash % 256 > 128 ? 1 : 0;
|
||||
const evolution = neighbors >= 3 && neighbors <= 5 ? 1 : cellState;
|
||||
processedValue = evolution * 255;
|
||||
break;
|
||||
}
|
||||
@ -668,7 +679,7 @@ class ShaderWorker {
|
||||
const noise3 = Math.sin(nx * 25.12) * Math.cos(ny * 25.12) * 0.25;
|
||||
|
||||
const combinedNoise = (noise1 + noise2 + noise3) / 1.75;
|
||||
processedValue = Math.floor(((combinedNoise + 1) * 0.5) * 255);
|
||||
processedValue = Math.floor((combinedNoise + 1) * 0.5 * 255);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -682,8 +693,12 @@ class ShaderWorker {
|
||||
const warpFreq = 0.02;
|
||||
|
||||
// Calculate warped coordinates
|
||||
const warpX = x + Math.sin(y * warpFreq + Math.abs(value) * 0.01) * warpStrength * 100;
|
||||
const warpY = y + Math.cos(x * warpFreq + Math.abs(value) * 0.01) * warpStrength * 100;
|
||||
const warpX =
|
||||
x +
|
||||
Math.sin(y * warpFreq + Math.abs(value) * 0.01) * warpStrength * 100;
|
||||
const warpY =
|
||||
y +
|
||||
Math.cos(x * warpFreq + Math.abs(value) * 0.01) * warpStrength * 100;
|
||||
|
||||
// Create barrel/lens distortion
|
||||
const dx = warpX - centerX;
|
||||
@ -693,7 +708,8 @@ class ShaderWorker {
|
||||
const normDist = dist / maxDist;
|
||||
|
||||
// Apply non-linear space deformation
|
||||
const deform = 1 + Math.sin(normDist * Math.PI + Math.abs(value) * 0.05) * 0.3;
|
||||
const deform =
|
||||
1 + Math.sin(normDist * Math.PI + Math.abs(value) * 0.05) * 0.3;
|
||||
const deformedX = centerX + dx * deform;
|
||||
const deformedY = centerY + dy * deform;
|
||||
|
||||
@ -713,18 +729,18 @@ class ShaderWorker {
|
||||
{
|
||||
x: centerX + Math.sin(Math.abs(value) * 0.01) * 200,
|
||||
y: centerY + Math.cos(Math.abs(value) * 0.01) * 200,
|
||||
strength: 1 + Math.abs(value) * 0.01
|
||||
strength: 1 + Math.abs(value) * 0.01,
|
||||
},
|
||||
{
|
||||
x: centerX + Math.cos(Math.abs(value) * 0.015) * 150,
|
||||
y: centerY + Math.sin(Math.abs(value) * 0.015) * 150,
|
||||
strength: -0.8 + Math.sin(Math.abs(value) * 0.02) * 0.5
|
||||
strength: -0.8 + Math.sin(Math.abs(value) * 0.02) * 0.5,
|
||||
},
|
||||
{
|
||||
x: centerX + Math.sin(Math.abs(value) * 0.008) * 300,
|
||||
y: centerY + Math.cos(Math.abs(value) * 0.012) * 250,
|
||||
strength: 0.6 + Math.cos(Math.abs(value) * 0.018) * 0.4
|
||||
}
|
||||
strength: 0.6 + Math.cos(Math.abs(value) * 0.018) * 0.4,
|
||||
},
|
||||
];
|
||||
|
||||
// Calculate flow field at this point
|
||||
@ -746,8 +762,8 @@ class ShaderWorker {
|
||||
|
||||
// Curl component (rotation) - creates vortices
|
||||
const curlStrength = source.strength * 0.5;
|
||||
flowX += (-dy / normalizedDist) * curlStrength / normalizedDist;
|
||||
flowY += (dx / normalizedDist) * curlStrength / normalizedDist;
|
||||
flowX += ((-dy / normalizedDist) * curlStrength) / normalizedDist;
|
||||
flowY += ((dx / normalizedDist) * curlStrength) / normalizedDist;
|
||||
}
|
||||
|
||||
// Add global flow influenced by value
|
||||
@ -757,7 +773,8 @@ class ShaderWorker {
|
||||
|
||||
// Add turbulence
|
||||
const turbScale = 0.05;
|
||||
const turbulence = Math.sin(x * turbScale + Math.abs(value) * 0.01) *
|
||||
const turbulence =
|
||||
Math.sin(x * turbScale + Math.abs(value) * 0.01) *
|
||||
Math.cos(y * turbScale + Math.abs(value) * 0.015) *
|
||||
(Math.abs(value) * 0.02);
|
||||
|
||||
@ -786,8 +803,10 @@ class ShaderWorker {
|
||||
|
||||
// Curl
|
||||
const curlStrength = source.strength * 0.5;
|
||||
localFlowX += (-dy / normalizedDist) * curlStrength / normalizedDist;
|
||||
localFlowY += (dx / normalizedDist) * curlStrength / normalizedDist;
|
||||
localFlowX +=
|
||||
((-dy / normalizedDist) * curlStrength) / normalizedDist;
|
||||
localFlowY +=
|
||||
((dx / normalizedDist) * curlStrength) / normalizedDist;
|
||||
}
|
||||
|
||||
// Move particle
|
||||
@ -798,11 +817,14 @@ class ShaderWorker {
|
||||
|
||||
// Calculate final value based on particle's final position and flow magnitude
|
||||
const flowMagnitude = Math.sqrt(flowX * flowX + flowY * flowY);
|
||||
const particleDistance = Math.sqrt((particleX - x) * (particleX - x) + (particleY - y) * (particleY - y));
|
||||
const particleDistance = Math.sqrt(
|
||||
(particleX - x) * (particleX - x) + (particleY - y) * (particleY - y)
|
||||
);
|
||||
|
||||
// Combine flow magnitude with particle trajectory
|
||||
const flowValue = (flowMagnitude * 10 + particleDistance * 2) % 256;
|
||||
const enhanced = Math.sin(flowValue * 0.05 + Math.abs(value) * 0.01) * 0.5 + 0.5;
|
||||
const enhanced =
|
||||
Math.sin(flowValue * 0.05 + Math.abs(value) * 0.01) * 0.5 + 0.5;
|
||||
|
||||
processedValue = Math.floor(enhanced * 255);
|
||||
break;
|
||||
@ -825,7 +847,6 @@ class ShaderWorker {
|
||||
return calculateColorDirect(processedValue, renderMode);
|
||||
}
|
||||
|
||||
|
||||
private sanitizeCode(code: string): string {
|
||||
// Auto-prefix Math functions
|
||||
const mathFunctions = [
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { AppSettings } from './stores/appSettings';
|
||||
import { STORAGE_KEYS, PERFORMANCE, DEFAULTS, ValueMode } from './utils/constants';
|
||||
import {
|
||||
STORAGE_KEYS,
|
||||
PERFORMANCE,
|
||||
DEFAULTS,
|
||||
ValueMode,
|
||||
} from './utils/constants';
|
||||
|
||||
export interface SavedShader {
|
||||
id: string;
|
||||
|
||||
@ -80,7 +80,7 @@ export function HelpPopup() {
|
||||
<strong>n</strong> - Noise value (0.0 to 1.0)
|
||||
</p>
|
||||
<p>
|
||||
<strong>b</strong> - Previous frame's value (feedback)
|
||||
<strong>b</strong> - Previous frame's value (feedback)
|
||||
</p>
|
||||
<p>
|
||||
<strong>mouseX, mouseY</strong> - Mouse position (0.0 to 1.0)
|
||||
@ -139,7 +139,7 @@ export function HelpPopup() {
|
||||
<p>
|
||||
<strong>trebleLevel</strong> - High frequencies (0.0-1.0)
|
||||
</p>
|
||||
<p>Click "Enable Audio" to activate microphone</p>
|
||||
<p>Click "Enable Audio" to activate microphone</p>
|
||||
</div>
|
||||
|
||||
<div className="help-section">
|
||||
@ -295,10 +295,7 @@ export function HelpPopup() {
|
||||
</p>
|
||||
<p>
|
||||
Website:{' '}
|
||||
<a
|
||||
href="https://raphaelforment.fr"
|
||||
target="_blank"
|
||||
>
|
||||
<a href="https://raphaelforment.fr" target="_blank" rel="noreferrer">
|
||||
raphaelforment.fr
|
||||
</a>
|
||||
</p>
|
||||
@ -307,6 +304,7 @@ export function HelpPopup() {
|
||||
<a
|
||||
href="https://git.raphaelforment.fr"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
git.raphaelforment.fr
|
||||
</a>
|
||||
|
||||
@ -112,7 +112,9 @@ export function MobileMenu() {
|
||||
<label>Value Mode</label>
|
||||
<select
|
||||
value={settings.valueMode}
|
||||
onChange={(e) => updateAppSettings({ valueMode: e.target.value as ValueMode })}
|
||||
onChange={(e) =>
|
||||
updateAppSettings({ valueMode: e.target.value as ValueMode })
|
||||
}
|
||||
>
|
||||
{VALUE_MODES.map((mode) => (
|
||||
<option key={mode} value={mode}>
|
||||
|
||||
@ -60,17 +60,24 @@ export function TopBar() {
|
||||
uiOpacity: settings.uiOpacity,
|
||||
};
|
||||
|
||||
try {
|
||||
const encoded = btoa(JSON.stringify(shareData));
|
||||
window.location.hash = encoded;
|
||||
const url = `${window.location.origin}${window.location.pathname}#${encoded}`;
|
||||
|
||||
console.log('Sharing URL:', url);
|
||||
console.log('Share data:', shareData);
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(window.location.href)
|
||||
.writeText(url)
|
||||
.then(() => {
|
||||
console.log('URL copied to clipboard');
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('Copy failed');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create share URL:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportPNG = () => {
|
||||
@ -151,7 +158,9 @@ export function TopBar() {
|
||||
Value Mode:
|
||||
<select
|
||||
value={settings.valueMode}
|
||||
onChange={(e) => updateAppSettings({ valueMode: e.target.value as ValueMode })}
|
||||
onChange={(e) =>
|
||||
updateAppSettings({ valueMode: e.target.value as ValueMode })
|
||||
}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
border: '1px solid #555',
|
||||
|
||||
@ -32,11 +32,17 @@ export const WelcomePopup: React.FC = () => {
|
||||
<h2 className="welcome-title">Welcome to BitFielder</h2>
|
||||
|
||||
<div className="welcome-content">
|
||||
<p>BitFielder is an experimental lofi bitfield shader editor made by <a href="https://raphaelforment.fr">BuboBubo</a>. Use it to create visual compositions through code. I use it for fun :) </p>
|
||||
<p>
|
||||
BitFielder is an experimental lofi bitfield shader editor made by{' '}
|
||||
<a href="https://raphaelforment.fr">BuboBubo</a>. Use it to create
|
||||
visual compositions through code. I use it for fun :){' '}
|
||||
</p>
|
||||
|
||||
<h3>Getting Started</h3>
|
||||
<ul>
|
||||
<li>Edit the shader code and press <i>Eval</i> or <i>Ctrl+Enter</i></li>
|
||||
<li>
|
||||
Edit the shader code and press <i>Eval</i> or <i>Ctrl+Enter</i>
|
||||
</li>
|
||||
<li>Use special variables to create reactive effects</li>
|
||||
<li>Explore/store shaders in the library (left pane)</li>
|
||||
<li>Export your creations as images or sharable links</li>
|
||||
@ -44,13 +50,24 @@ export const WelcomePopup: React.FC = () => {
|
||||
|
||||
<h3>Key Features</h3>
|
||||
<ul>
|
||||
<li><strong>Real-time editing:</strong> See your changes instantly</li>
|
||||
<li><strong>Motion and touch:</strong> Mouse, touchscreen support</li>
|
||||
<li><strong>Audio reactive:</strong> Synchronize with a sound signal</li>
|
||||
<li><strong>Export capabilities:</strong> Save and share your work</li>
|
||||
<li>
|
||||
<strong>Real-time editing:</strong> See your changes instantly
|
||||
</li>
|
||||
<li>
|
||||
<strong>Motion and touch:</strong> Mouse, touchscreen support
|
||||
</li>
|
||||
<li>
|
||||
<strong>Audio reactive:</strong> Synchronize with a sound signal
|
||||
</li>
|
||||
<li>
|
||||
<strong>Export capabilities:</strong> Save and share your work
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p className="help-hint">Press <kbd>?</kbd> anytime to view keyboard shortcuts and detailed help.</p>
|
||||
<p className="help-hint">
|
||||
Press <kbd>?</kbd> anytime to view keyboard shortcuts and detailed
|
||||
help.
|
||||
</p>
|
||||
|
||||
<p className="dismiss-hint">Press any key to dismiss this message</p>
|
||||
</div>
|
||||
|
||||
14
src/main.tsx
14
src/main.tsx
@ -13,11 +13,20 @@ $appSettings.set(savedSettings);
|
||||
function loadFromURL() {
|
||||
if (window.location.hash) {
|
||||
try {
|
||||
const decoded = atob(window.location.hash.substring(1));
|
||||
const hash = window.location.hash.substring(1);
|
||||
console.log('Loading from URL hash:', hash);
|
||||
|
||||
const decoded = atob(hash);
|
||||
console.log('Decoded data:', decoded);
|
||||
|
||||
try {
|
||||
const shareData = JSON.parse(decoded);
|
||||
console.log('Parsed share data:', shareData);
|
||||
|
||||
if (shareData.code) {
|
||||
setShaderCode(shareData.code);
|
||||
}
|
||||
|
||||
$appSettings.set({
|
||||
resolution: shareData.resolution || savedSettings.resolution,
|
||||
fps: shareData.fps || savedSettings.fps,
|
||||
@ -28,7 +37,10 @@ function loadFromURL() {
|
||||
? shareData.uiOpacity
|
||||
: savedSettings.uiOpacity,
|
||||
});
|
||||
|
||||
console.log('Settings updated from URL');
|
||||
} catch (jsonError) {
|
||||
console.log('JSON parse failed, falling back to old format');
|
||||
// Fall back to old format (just code as string)
|
||||
setShaderCode(decoded);
|
||||
}
|
||||
|
||||
@ -21,7 +21,10 @@ export const defaultUIState: UIState = {
|
||||
export const uiState = atom<UIState>(defaultUIState);
|
||||
|
||||
export function toggleMobileMenu() {
|
||||
uiState.set({ ...uiState.get(), mobileMenuOpen: !uiState.get().mobileMenuOpen });
|
||||
uiState.set({
|
||||
...uiState.get(),
|
||||
mobileMenuOpen: !uiState.get().mobileMenuOpen,
|
||||
});
|
||||
}
|
||||
|
||||
export function closeMobileMenu() {
|
||||
@ -37,7 +40,10 @@ export function hideHelp() {
|
||||
}
|
||||
|
||||
export function toggleShaderLibrary() {
|
||||
uiState.set({ ...uiState.get(), shaderLibraryOpen: !uiState.get().shaderLibraryOpen });
|
||||
uiState.set({
|
||||
...uiState.get(),
|
||||
shaderLibraryOpen: !uiState.get().shaderLibraryOpen,
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleUI() {
|
||||
|
||||
@ -462,7 +462,7 @@ button [data-lucide] {
|
||||
}
|
||||
|
||||
.welcome-content li:before {
|
||||
content: "▸";
|
||||
content: '▸';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #999;
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
export function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
|
||||
export function hsvToRgb(
|
||||
h: number,
|
||||
s: number,
|
||||
v: number
|
||||
): [number, number, number] {
|
||||
const c = v * s;
|
||||
const x = c * (1 - Math.abs(((h * 6) % 2) - 1));
|
||||
const m = v - c;
|
||||
@ -97,11 +101,7 @@ export function cyberpunkColor(value: number): [number, number, number] {
|
||||
const t = value / 255.0;
|
||||
const phase = (t * 3) % 1;
|
||||
if (phase < 0.33) {
|
||||
return [
|
||||
Math.round(255 * (1 - phase * 3)),
|
||||
0,
|
||||
Math.round(255 * phase * 3),
|
||||
];
|
||||
return [Math.round(255 * (1 - phase * 3)), 0, Math.round(255 * phase * 3)];
|
||||
} else if (phase < 0.67) {
|
||||
const p = (phase - 0.33) * 3;
|
||||
return [0, Math.round(255 * p), Math.round(255 * (1 - p))];
|
||||
|
||||
@ -36,10 +36,10 @@ export const VALUE_MODES = [
|
||||
'cellular',
|
||||
'noise',
|
||||
'warp',
|
||||
'flow'
|
||||
'flow',
|
||||
] as const;
|
||||
|
||||
export type ValueMode = typeof VALUE_MODES[number];
|
||||
export type ValueMode = (typeof VALUE_MODES)[number];
|
||||
|
||||
// Default Values
|
||||
export const DEFAULTS = {
|
||||
|
||||
Reference in New Issue
Block a user