Skip to content

Instantly share code, notes, and snippets.

@snowclipsed
Last active January 12, 2025 20:59
Show Gist options
  • Select an option

  • Save snowclipsed/8555c658e51f4f88fc10fd000588b86f to your computer and use it in GitHub Desktop.

Select an option

Save snowclipsed/8555c658e51f4f88fc10fd000588b86f to your computer and use it in GitHub Desktop.

Revisions

  1. snowclipsed revised this gist Dec 31, 2024. 1 changed file with 156 additions and 156 deletions.
    312 changes: 156 additions & 156 deletions CyberpunkLorenz.tsx
    Original file line number Diff line number Diff line change
    @@ -1,107 +1,92 @@
    import React, { useState, useEffect, useCallback, useRef } from 'react';

    /**
    * Represents a point in 3D space within the Lorenz system.
    */
    interface Point {
    x: number;
    y: number;
    z: number;
    }

    /**
    * Represents a point projected onto a 2D plane for display.
    */
    interface ProjectedPoint {
    x: number;
    y: number;
    }

    /**
    * Configuration parameters for the Lorenz system and visualization.
    */
    interface Config {
    scale: number;
    displayScale: number;
    xOffset: number;
    yOffset: number;
    rotateX: number;
    rotateY: number;
    rotateZ: number;
    sigma: number;
    rho: number;
    beta: number;
    speed: number;
    scale: number; // Base scale factor for the visualization
    displayScale: number; // Additional scale factor for display adjustment
    xOffset: number; // Horizontal offset for centering
    yOffset: number; // Vertical offset for centering
    rotateX: number; // Rotation around X axis (degrees)
    rotateY: number; // Rotation around Y axis (degrees)
    rotateZ: number; // Rotation around Z axis (degrees)
    sigma: number; // σ parameter of the Lorenz system
    rho: number; // ρ parameter of the Lorenz system
    beta: number; // β parameter of the Lorenz system
    speed: number; // Animation speed multiplier
    }

    /**
    * Configuration for the control sliders.
    */
    interface SliderConfig {
    key: keyof Config;
    jpLabel: string;
    enLabel: string;
    min: number;
    max: number;
    step: number;
    jpLabel: string; // Japanese label
    enLabel: string; // English label
    min: number; // Minimum value
    max: number; // Maximum value
    step: number; // Step size
    }

    /**
    * CyberpunkLorenz is a React functional component that renders an interactive
    * Lorenz attractor visualization in ASCII art style. The component allows users
    * to rotate and zoom the visualization using mouse interactions and provides
    * sliders to adjust various parameters of the Lorenz system.
    *
    * @component
    * @example
    * return (
    * <CyberpunkLorenz />
    * )
    *
    * @typedef {Object} Point
    * @property {number} x - The x-coordinate of the point.
    * @property {number} y - The y-coordinate of the point.
    * @property {number} z - The z-coordinate of the point.
    *
    * @typedef {Object} ProjectedPoint
    * @property {number} x - The x-coordinate of the projected point.
    * @property {number} y - The y-coordinate of the projected point.
    *
    * @typedef {Object} Config
    * @property {number} scale - The scale factor for the Lorenz attractor.
    * @property {number} displayScale - The display scale factor for the visualization.
    * @property {number} xOffset - The x-offset for the visualization.
    * @property {number} yOffset - The y-offset for the visualization.
    * @property {number} rotateX - The rotation angle around the X-axis.
    * @property {number} rotateY - The rotation angle around the Y-axis.
    * @property {number} rotateZ - The rotation angle around the Z-axis.
    * @property {number} sigma - The sigma parameter of the Lorenz system.
    * @property {number} rho - The rho parameter of the Lorenz system.
    * @property {number} beta - The beta parameter of the Lorenz system.
    * @property {number} speed - The speed factor for the animation.
    *
    * @typedef {Object} SliderConfig
    * @property {keyof Config} key - The key of the configuration parameter.
    * @property {string} jpLabel - The Japanese label for the slider.
    * @property {string} enLabel - The English label for the slider.
    * @property {number} min - The minimum value for the slider.
    * @property {number} max - The maximum value for the slider.
    * @property {number} step - The step value for the slider.
    *
    * @returns {React.ReactElement} The rendered CyberpunkLorenz component.
    * Default configuration values for the Lorenz system and visualization.
    * These values create a stable and visually pleasing initial state.
    */
    const CyberpunkLorenz: React.FC<{}> = () => {
    const DEFAULT_CONFIG: Config = {
    scale: 2,
    displayScale: 0.9,
    xOffset: 80,
    yOffset: 30,
    rotateX: 24.0,
    rotateY: -18.0,
    rotateZ: -44.0,
    sigma: 10.3,
    rho: 23.7,
    beta: 1.7,
    speed: 1.0
    };

    /**
    * CyberpunkLorenz is a React component that renders an interactive ASCII art
    * visualization of the Lorenz attractor. It supports rotation, zooming, and
    * real-time parameter adjustment through a control panel.
    */
    const CyberpunkLorenz = () => {
    // State management for points and interaction
    const [points, setPoints] = useState<Point[]>([]);
    const [isDragging, setIsDragging] = useState(false);
    const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 });
    const [isHeatmap, setIsHeatmap] = useState(false);
    const containerRef = useRef<HTMLDivElement>(null);

    const [config, setConfig] = useState<Config>({
    scale: 2,
    displayScale: 0.9,
    xOffset: 80,
    yOffset: 30,
    rotateX: 24.0,
    rotateY: -18.0,
    rotateZ: -44.0,
    sigma: 10.3,
    rho: 23.7,
    beta: 1.7,
    speed: 1.0
    });
    // Initial configuration state
    const [config, setConfig] = useState<Config>(DEFAULT_CONFIG);

    const animationRef = useRef<number | null>(null);
    const dt = 0.01;
    const dt = 0.01; // Time step for numerical integration

    /**
    * Handles the start of a mouse drag operation.
    * Initializes dragging state and stores the initial mouse position.
    */
    const handleMouseDown = (e: React.MouseEvent) => {
    setIsDragging(true);
    setLastMousePos({
    @@ -110,12 +95,15 @@ const CyberpunkLorenz: React.FC<{}> = () => {
    });
    };

    /**
    * Handles mouse movement during drag operations.
    * Updates rotation angles based on mouse movement delta.
    */
    const handleMouseMove = (e: React.MouseEvent) => {
    if (!isDragging) return;

    const deltaX = e.clientX - lastMousePos.x;
    const deltaY = e.clientY - lastMousePos.y;

    const rotationSpeed = 0.5;

    setConfig(prev => ({
    @@ -130,14 +118,10 @@ const CyberpunkLorenz: React.FC<{}> = () => {
    });
    };

    const handleMouseUp = () => {
    setIsDragging(false);
    };

    const handleMouseLeave = () => {
    setIsDragging(false);
    };

    /**
    * Handles mouse scroll wheel events for zooming.
    * Updates the display scale based on scroll direction.
    */
    const handleWheel = (e: React.WheelEvent) => {
    e.preventDefault();
    const zoomSpeed = 0.05;
    @@ -149,33 +133,43 @@ const CyberpunkLorenz: React.FC<{}> = () => {
    }));
    };


    /**
    * Rotates a given point in 3D space based on the current configuration's rotation angles.
    *
    * @param point - The point to be rotated, represented by its x, y, and z coordinates.
    * @returns The rotated point with updated x, y, and z coordinates.
    * Rotates a point in 3D space according to the current rotation angles.
    * Applies rotations around X, Y, and Z axes in sequence.
    *
    * @param point - The point to rotate
    * @returns The rotated point
    */
    const rotatePoint = (point: Point): Point => {
    const { x, y, z } = point;
    const radX = config.rotateX * Math.PI / 180;
    const radY = config.rotateY * Math.PI / 180;
    const radZ = config.rotateZ * Math.PI / 180;

    // Rotate around X axis
    const x1 = x;
    const y1 = y * Math.cos(radX) - z * Math.sin(radX);
    const z1 = y * Math.sin(radX) + z * Math.cos(radX);

    // Rotate around Y axis
    const x2 = x1 * Math.cos(radY) + z1 * Math.sin(radY);
    const y2 = y1;
    const z2 = -x1 * Math.sin(radY) + z1 * Math.cos(radY);

    // Rotate around Z axis
    const x3 = x2 * Math.cos(radZ) - y2 * Math.sin(radZ);
    const y3 = x2 * Math.sin(radZ) + y2 * Math.cos(radZ);

    return { x: x3, y: y3, z: z2 };
    };

    /**
    * Projects a 3D point onto the 2D display plane.
    * Applies rotation, scaling, and offset transformations.
    *
    * @param point - The 3D point to project
    * @returns The projected 2D point
    */
    const project = (point: Point): ProjectedPoint => {
    const rotated = rotatePoint(point);
    return {
    @@ -184,70 +178,76 @@ const CyberpunkLorenz: React.FC<{}> = () => {
    };
    };


    /**
    * Determines the color class for a given point based on its position and the current heatmap setting.
    *
    * @param point - The point for which to determine the color class.
    * @returns A string representing the CSS class for the color of the point.
    *
    * If the heatmap is enabled, the color is determined based on the distance of the point from two predefined centers,
    * creating an "onion ring" effect. The closer the point is to a center, the warmer the color.
    * - Red for distances less than 2 units.
    * - Orange for distances between 2 and 4 units.
    * - Yellow for distances between 4 and 6 units.
    * - Green for distances between 6 and 8 units.
    * - Blue for distances between 8 and 10 units.
    * - Violet for distances greater than 10 units.
    *
    * If the heatmap is not enabled, the color is white.
    * Determines the color class for a point based on its position relative to
    * the Lorenz attractor's centers. Creates a concentric circle effect around
    * the attractor's "eyes".
    *
    * @param point - The point to color
    * @returns CSS class name for the point's color
    */
    const getColorClass = (point: Point): string => {
    const getColorClass = useCallback((point: Point): string => {
    if (isHeatmap) {
    const { x, y, z } = point;

    // Define the two attractor centers
    const center1 = { x: -8.5, y: -8.5 };
    const center2 = { x: 8.5, y: 8.5 };
    // Calculate attractor centers based on system parameters
    const center1 = {
    x: Math.sqrt(config.beta * (config.rho - 1)),
    y: Math.sqrt(config.beta * (config.rho - 1)),
    z: config.rho - 1
    };
    const center2 = {
    x: -Math.sqrt(config.beta * (config.rho - 1)),
    y: -Math.sqrt(config.beta * (config.rho - 1)),
    z: config.rho - 1
    };

    // Calculate distances to both centers
    const dist1 = Math.sqrt(Math.pow(x - center1.x, 2) + Math.pow(y - center1.y, 2));
    const dist2 = Math.sqrt(Math.pow(x - center2.x, 2) + Math.pow(y - center2.y, 2));
    // Calculate 3D distances to both centers
    const dist1 = Math.sqrt(
    Math.pow(x - center1.x, 2) +
    Math.pow(y - center1.y, 2) +
    Math.pow(z - center1.z, 2)
    );
    const dist2 = Math.sqrt(
    Math.pow(x - center2.x, 2) +
    Math.pow(y - center2.y, 2) +
    Math.pow(z - center2.z, 2)
    );

    // Use the smaller distance for coloring
    // Use minimum distance and normalize
    const dist = Math.min(dist1, dist2);
    const maxDist = Math.sqrt(
    Math.pow(center1.x - center2.x, 2) +
    Math.pow(center1.y - center2.y, 2) +
    Math.pow(center1.z - center2.z, 2)
    );

    // Color based on distance from nearest center (onion ring effect)
    if (dist < 2) {
    const normalizedDist = dist / maxDist;

    // Apply color gradients based on normalized distance
    if (normalizedDist < 0.15) {
    return 'text-red-500';
    } else if (dist < 4) {
    } else if (normalizedDist < 0.3) {
    return 'text-orange-500';
    } else if (dist < 6) {
    } else if (normalizedDist < 0.45) {
    return 'text-yellow-500';
    } else if (dist < 8) {
    } else if (normalizedDist < 0.6) {
    return 'text-green-500';
    } else if (dist < 10) {
    } else if (normalizedDist < 0.75) {
    return 'text-blue-500';
    } else {
    return 'text-violet-500';
    }
    }
    return 'text-white';
    };
    }, [isHeatmap, config.beta, config.rho]);

    /**
    * Generates an ASCII frame from a set of points and returns it as a React element.
    *
    * @param points - An array of Point objects to be projected and rendered.
    * @returns A React element representing the ASCII frame.
    *
    * The function creates a 2D buffer of characters and a corresponding color buffer.
    * Each point is projected and scaled according to the display scale configuration.
    * If the scaled coordinates fall within the buffer dimensions, the character and color
    * at that position are updated.
    * Creates an ASCII art frame from the current set of points.
    * Projects 3D points to 2D and assigns appropriate characters and colors.
    *
    * The resulting buffers are then mapped to a series of div and span elements to create
    * the final ASCII frame, with appropriate classes for styling.
    * @param points - Array of 3D points to render
    * @returns React element containing the ASCII frame
    */
    const createAsciiFrame = useCallback((points: Point[]): React.ReactElement => {
    const width = 100;
    @@ -279,32 +279,12 @@ const CyberpunkLorenz: React.FC<{}> = () => {
    ))}
    </>
    );
    }, [config.displayScale, project, isHeatmap]);



    useEffect(() => {
    const element = containerRef.current;
    if (!element) return;

    const disableScroll = () => {
    document.body.style.overflow = 'hidden';
    };

    const enableScroll = () => {
    document.body.style.overflow = 'auto';
    };

    element.addEventListener('mouseenter', disableScroll);
    element.addEventListener('mouseleave', enableScroll);

    return () => {
    element.removeEventListener('mouseenter', disableScroll);
    element.removeEventListener('mouseleave', enableScroll);
    document.body.style.overflow = 'auto';
    };
    }, []);
    }, [config.displayScale, project, getColorClass]);

    /**
    * Initializes the Lorenz system with starting points.
    * Uses numerical integration to generate initial set of points.
    */
    useEffect(() => {
    let x = 0.1;
    let y = 0;
    @@ -326,6 +306,10 @@ const CyberpunkLorenz: React.FC<{}> = () => {
    setPoints(initialPoints);
    }, [config.sigma, config.rho, config.beta]);

    /**
    * Handles the animation loop for the Lorenz system.
    * Continuously updates points using the Lorenz equations.
    */
    useEffect(() => {
    const animate = () => {
    setPoints(prevPoints => {
    @@ -357,6 +341,16 @@ const CyberpunkLorenz: React.FC<{}> = () => {
    };
    }, [config]);

    /**
    * Controls for the visualization parameters
    */
    /**
    * Handler for configuration changes from the control sliders.
    * Updates the specified configuration parameter with the new value.
    *
    * @param key - The configuration parameter to update
    * @param value - The new value for the parameter
    */
    const handleConfigChange = (key: keyof Config, value: number): void => {
    setConfig(prev => ({ ...prev, [key]: value }));
    };
    @@ -377,8 +371,8 @@ const CyberpunkLorenz: React.FC<{}> = () => {
    ref={containerRef}
    onMouseDown={handleMouseDown}
    onMouseMove={handleMouseMove}
    onMouseUp={handleMouseUp}
    onMouseLeave={handleMouseLeave}
    onMouseUp={() => setIsDragging(false)}
    onMouseLeave={() => setIsDragging(false)}
    onWheel={handleWheel}
    className="flex-1 cursor-move border-r border-white"
    >
    @@ -397,6 +391,12 @@ const CyberpunkLorenz: React.FC<{}> = () => {
    >
    {isHeatmap ? 'Switch to White' : 'Switch to Rainbow'}
    </button>
    <button
    onClick={() => setConfig(DEFAULT_CONFIG)}
    className="mt-2 px-2 py-1 border border-white hover:bg-white hover:text-black transition-colors w-full"
    >
    Reset to Default
    </button>
    </div>
    {sliders.map(({ key, enLabel, min, max, step }) => (
    <div key={key} className="border border-white p-2 mt-1">
  2. snowclipsed created this gist Dec 31, 2024.
    423 changes: 423 additions & 0 deletions CyberpunkLorenz.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,423 @@
    import React, { useState, useEffect, useCallback, useRef } from 'react';

    interface Point {
    x: number;
    y: number;
    z: number;
    }

    interface ProjectedPoint {
    x: number;
    y: number;
    }

    interface Config {
    scale: number;
    displayScale: number;
    xOffset: number;
    yOffset: number;
    rotateX: number;
    rotateY: number;
    rotateZ: number;
    sigma: number;
    rho: number;
    beta: number;
    speed: number;
    }

    interface SliderConfig {
    key: keyof Config;
    jpLabel: string;
    enLabel: string;
    min: number;
    max: number;
    step: number;
    }

    /**
    * CyberpunkLorenz is a React functional component that renders an interactive
    * Lorenz attractor visualization in ASCII art style. The component allows users
    * to rotate and zoom the visualization using mouse interactions and provides
    * sliders to adjust various parameters of the Lorenz system.
    *
    * @component
    * @example
    * return (
    * <CyberpunkLorenz />
    * )
    *
    * @typedef {Object} Point
    * @property {number} x - The x-coordinate of the point.
    * @property {number} y - The y-coordinate of the point.
    * @property {number} z - The z-coordinate of the point.
    *
    * @typedef {Object} ProjectedPoint
    * @property {number} x - The x-coordinate of the projected point.
    * @property {number} y - The y-coordinate of the projected point.
    *
    * @typedef {Object} Config
    * @property {number} scale - The scale factor for the Lorenz attractor.
    * @property {number} displayScale - The display scale factor for the visualization.
    * @property {number} xOffset - The x-offset for the visualization.
    * @property {number} yOffset - The y-offset for the visualization.
    * @property {number} rotateX - The rotation angle around the X-axis.
    * @property {number} rotateY - The rotation angle around the Y-axis.
    * @property {number} rotateZ - The rotation angle around the Z-axis.
    * @property {number} sigma - The sigma parameter of the Lorenz system.
    * @property {number} rho - The rho parameter of the Lorenz system.
    * @property {number} beta - The beta parameter of the Lorenz system.
    * @property {number} speed - The speed factor for the animation.
    *
    * @typedef {Object} SliderConfig
    * @property {keyof Config} key - The key of the configuration parameter.
    * @property {string} jpLabel - The Japanese label for the slider.
    * @property {string} enLabel - The English label for the slider.
    * @property {number} min - The minimum value for the slider.
    * @property {number} max - The maximum value for the slider.
    * @property {number} step - The step value for the slider.
    *
    * @returns {React.ReactElement} The rendered CyberpunkLorenz component.
    */
    const CyberpunkLorenz: React.FC<{}> = () => {
    const [points, setPoints] = useState<Point[]>([]);
    const [isDragging, setIsDragging] = useState(false);
    const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 });
    const [isHeatmap, setIsHeatmap] = useState(false);
    const containerRef = useRef<HTMLDivElement>(null);

    const [config, setConfig] = useState<Config>({
    scale: 2,
    displayScale: 0.9,
    xOffset: 80,
    yOffset: 30,
    rotateX: 24.0,
    rotateY: -18.0,
    rotateZ: -44.0,
    sigma: 10.3,
    rho: 23.7,
    beta: 1.7,
    speed: 1.0
    });

    const animationRef = useRef<number | null>(null);
    const dt = 0.01;

    const handleMouseDown = (e: React.MouseEvent) => {
    setIsDragging(true);
    setLastMousePos({
    x: e.clientX,
    y: e.clientY
    });
    };

    const handleMouseMove = (e: React.MouseEvent) => {
    if (!isDragging) return;

    const deltaX = e.clientX - lastMousePos.x;
    const deltaY = e.clientY - lastMousePos.y;

    const rotationSpeed = 0.5;

    setConfig(prev => ({
    ...prev,
    rotateY: prev.rotateY + deltaX * rotationSpeed,
    rotateX: prev.rotateX + deltaY * rotationSpeed
    }));

    setLastMousePos({
    x: e.clientX,
    y: e.clientY
    });
    };

    const handleMouseUp = () => {
    setIsDragging(false);
    };

    const handleMouseLeave = () => {
    setIsDragging(false);
    };

    const handleWheel = (e: React.WheelEvent) => {
    e.preventDefault();
    const zoomSpeed = 0.05;
    const delta = e.deltaY > 0 ? -zoomSpeed : zoomSpeed;

    setConfig(prev => ({
    ...prev,
    displayScale: Math.max(0.5, Math.min(2.5, prev.displayScale + delta))
    }));
    };


    /**
    * Rotates a given point in 3D space based on the current configuration's rotation angles.
    *
    * @param point - The point to be rotated, represented by its x, y, and z coordinates.
    * @returns The rotated point with updated x, y, and z coordinates.
    */
    const rotatePoint = (point: Point): Point => {
    const { x, y, z } = point;
    const radX = config.rotateX * Math.PI / 180;
    const radY = config.rotateY * Math.PI / 180;
    const radZ = config.rotateZ * Math.PI / 180;

    const x1 = x;
    const y1 = y * Math.cos(radX) - z * Math.sin(radX);
    const z1 = y * Math.sin(radX) + z * Math.cos(radX);

    const x2 = x1 * Math.cos(radY) + z1 * Math.sin(radY);
    const y2 = y1;
    const z2 = -x1 * Math.sin(radY) + z1 * Math.cos(radY);

    const x3 = x2 * Math.cos(radZ) - y2 * Math.sin(radZ);
    const y3 = x2 * Math.sin(radZ) + y2 * Math.cos(radZ);

    return { x: x3, y: y3, z: z2 };
    };

    const project = (point: Point): ProjectedPoint => {
    const rotated = rotatePoint(point);
    return {
    x: Math.floor(rotated.x * config.scale + config.xOffset),
    y: Math.floor(rotated.y * config.scale + config.yOffset)
    };
    };


    /**
    * Determines the color class for a given point based on its position and the current heatmap setting.
    *
    * @param point - The point for which to determine the color class.
    * @returns A string representing the CSS class for the color of the point.
    *
    * If the heatmap is enabled, the color is determined based on the distance of the point from two predefined centers,
    * creating an "onion ring" effect. The closer the point is to a center, the warmer the color.
    * - Red for distances less than 2 units.
    * - Orange for distances between 2 and 4 units.
    * - Yellow for distances between 4 and 6 units.
    * - Green for distances between 6 and 8 units.
    * - Blue for distances between 8 and 10 units.
    * - Violet for distances greater than 10 units.
    *
    * If the heatmap is not enabled, the color is white.
    */
    const getColorClass = (point: Point): string => {
    if (isHeatmap) {
    const { x, y, z } = point;

    // Define the two attractor centers
    const center1 = { x: -8.5, y: -8.5 };
    const center2 = { x: 8.5, y: 8.5 };

    // Calculate distances to both centers
    const dist1 = Math.sqrt(Math.pow(x - center1.x, 2) + Math.pow(y - center1.y, 2));
    const dist2 = Math.sqrt(Math.pow(x - center2.x, 2) + Math.pow(y - center2.y, 2));

    // Use the smaller distance for coloring
    const dist = Math.min(dist1, dist2);

    // Color based on distance from nearest center (onion ring effect)
    if (dist < 2) {
    return 'text-red-500';
    } else if (dist < 4) {
    return 'text-orange-500';
    } else if (dist < 6) {
    return 'text-yellow-500';
    } else if (dist < 8) {
    return 'text-green-500';
    } else if (dist < 10) {
    return 'text-blue-500';
    } else {
    return 'text-violet-500';
    }
    }
    return 'text-white';
    };

    /**
    * Generates an ASCII frame from a set of points and returns it as a React element.
    *
    * @param points - An array of Point objects to be projected and rendered.
    * @returns A React element representing the ASCII frame.
    *
    * The function creates a 2D buffer of characters and a corresponding color buffer.
    * Each point is projected and scaled according to the display scale configuration.
    * If the scaled coordinates fall within the buffer dimensions, the character and color
    * at that position are updated.
    *
    * The resulting buffers are then mapped to a series of div and span elements to create
    * the final ASCII frame, with appropriate classes for styling.
    */
    const createAsciiFrame = useCallback((points: Point[]): React.ReactElement => {
    const width = 100;
    const height = 45;
    const buffer: string[][] = Array(height).fill(null).map(() => Array(width).fill(' '));
    const colorBuffer: string[][] = Array(height).fill(null).map(() => Array(width).fill('text-white'));

    points.forEach(point => {
    const projected = project(point);
    const scaledX = Math.floor(projected.x * config.displayScale);
    const scaledY = Math.floor(projected.y * config.displayScale);

    if (scaledX >= 0 && scaledX < width && scaledY >= 0 && scaledY < height) {
    buffer[scaledY][scaledX] = '█';
    colorBuffer[scaledY][scaledX] = getColorClass(point);
    }
    });

    return (
    <>
    {buffer.map((row, i) => (
    <div key={i} className="whitespace-pre">
    {row.map((char, j) => (
    <span key={`${i}-${j}`} className={colorBuffer[i][j]}>
    {char}
    </span>
    ))}
    </div>
    ))}
    </>
    );
    }, [config.displayScale, project, isHeatmap]);



    useEffect(() => {
    const element = containerRef.current;
    if (!element) return;

    const disableScroll = () => {
    document.body.style.overflow = 'hidden';
    };

    const enableScroll = () => {
    document.body.style.overflow = 'auto';
    };

    element.addEventListener('mouseenter', disableScroll);
    element.addEventListener('mouseleave', enableScroll);

    return () => {
    element.removeEventListener('mouseenter', disableScroll);
    element.removeEventListener('mouseleave', enableScroll);
    document.body.style.overflow = 'auto';
    };
    }, []);

    useEffect(() => {
    let x = 0.1;
    let y = 0;
    let z = 0;
    const initialPoints: Point[] = [];

    for(let i = 0; i < 1000; i++) {
    const dx = config.sigma * (y - x) * dt;
    const dy = (x * (config.rho - z) - y) * dt;
    const dz = (x * y - config.beta * z) * dt;

    x += dx;
    y += dy;
    z += dz;

    initialPoints.push({ x, y, z });
    }

    setPoints(initialPoints);
    }, [config.sigma, config.rho, config.beta]);

    useEffect(() => {
    const animate = () => {
    setPoints(prevPoints => {
    const newPoints = [...prevPoints];
    const last = newPoints[newPoints.length - 1];

    const dx = config.sigma * (last.y - last.x) * dt * config.speed;
    const dy = (last.x * (config.rho - last.z) - last.y) * dt * config.speed;
    const dz = (last.x * last.y - config.beta * last.z) * dt * config.speed;

    newPoints.push({
    x: last.x + dx,
    y: last.y + dy,
    z: last.z + dz
    });

    if (newPoints.length > 1000) newPoints.shift();
    return newPoints;
    });

    animationRef.current = requestAnimationFrame(animate);
    };

    animate();
    return () => {
    if (animationRef.current !== null) {
    cancelAnimationFrame(animationRef.current);
    }
    };
    }, [config]);

    const handleConfigChange = (key: keyof Config, value: number): void => {
    setConfig(prev => ({ ...prev, [key]: value }));
    };

    const sliders: SliderConfig[] = [
    { key: 'rotateX', jpLabel: 'X軸', enLabel: 'X Axis', min: -180, max: 180, step: 1 },
    { key: 'rotateY', jpLabel: 'Y軸', enLabel: 'Y Axis', min: -180, max: 180, step: 1 },
    { key: 'rotateZ', jpLabel: 'Z軸', enLabel: 'Z Axis', min: -180, max: 180, step: 1 },
    { key: 'sigma', jpLabel: 'σ', enLabel: 'Sigma', min: 1, max: 20, step: 0.1 },
    { key: 'rho', jpLabel: 'ρ', enLabel: 'Rho', min: 0, max: 50, step: 0.1 },
    { key: 'beta', jpLabel: 'β', enLabel: 'Beta', min: 0, max: 10, step: 0.1 },
    { key: 'speed', jpLabel: '速度', enLabel: 'Speed', min: 0.1, max: 3, step: 0.1 }
    ];

    return (
    <div className="flex border border-white bg-black h-full text-white">
    <div
    ref={containerRef}
    onMouseDown={handleMouseDown}
    onMouseMove={handleMouseMove}
    onMouseUp={handleMouseUp}
    onMouseLeave={handleMouseLeave}
    onWheel={handleWheel}
    className="flex-1 cursor-move border-r border-white"
    >
    <div className="font-mono text-[0.6rem] leading-none p-2 h-full select-none">
    {createAsciiFrame(points)}
    </div>
    </div>

    <div className="w-44 flex flex-col p-2 text-[0.6rem] shrink-0">
    <div className="border border-white p-2 mb-1 text-center">
    <p>Click and drag to rotate</p>
    <p>Use mouse wheel to zoom</p>
    <button
    onClick={() => setIsHeatmap(prev => !prev)}
    className="mt-2 px-2 py-1 border border-white hover:bg-white hover:text-black transition-colors w-full"
    >
    {isHeatmap ? 'Switch to White' : 'Switch to Rainbow'}
    </button>
    </div>
    {sliders.map(({ key, enLabel, min, max, step }) => (
    <div key={key} className="border border-white p-2 mt-1">
    <div className="flex justify-between mb-1">
    <span>{enLabel}</span>
    <span>{config[key].toFixed(1)}</span>
    </div>
    <input
    type="range"
    min={min}
    max={max}
    step={step}
    value={config[key]}
    onChange={(e) => handleConfigChange(key, parseFloat(e.target.value))}
    className="w-full accent-white"
    />
    </div>
    ))}
    </div>
    </div>
    );
    };

    export default CyberpunkLorenz;