Rothko Generator
7/25/2024
Choose Colors
#C92A2A
#A295EA
#F59F00
Choose Heights
Adjust the height proportion for Block 1
Adjust the height proportion for Block 2
Adjust the height proportion for Block 3
Choose Brownian Intensity
Select the intensity of the Brownian motion
Choose Padding Amounts
Select the padding amount
Select the inner padding amount
import React, { useEffect, useRef, useState, useCallback } from 'react';
import * as fabric from 'fabric';
import { FabricJSCanvas, useFabricJSEditor } from 'fabricjs-react';
import { ColorPicker, Title, Text, Space, NumberInput } from '@mantine/core';
import init, { create_brownian_path } from '../wasm/pkg/wasm_rothko';
import TextSpoiler from './TextSpoiler';
import CodeH from './CodeH';
// Debounce utility function
function debounce<T extends (...args: any[]) => void>(func: T, wait: number) {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
const RothkoCanvas: React.FC = () => {
const { editor, onReady } = useFabricJSEditor();
const containerRef = useRef<HTMLDivElement>(null);
// State to hold the colors for the rectangles
const [colors, setColors] = useState([
{ color: '#C92A2A', height: 0.28 },
{ color: '#A295EA', height: 0.42 },
{ color: '#F59F00', height: 0.23 },
]);
// State for Brownian motion intensity and padding amounts
const [brownianIntensity, setBrownianIntensity] = useState(5);
const [padding, setPadding] = useState(65);
const [innerPadding, setInnerPadding] = useState(75);
useEffect(() => {
const handleResize = async () => {
await init(); // Initialize the Wasm module
if (editor) {
const canvas = editor.canvas;
const container = containerRef.current;
if (container) {
const width = container.clientWidth;
const height = container.clientHeight;
canvas.setWidth(width);
canvas.setHeight(height);
// Clear the canvas before drawing
canvas.clear();
// Function to add multiple Brownian lines spewing off the edges
const addBrownianLines = (
canvas: fabric.Canvas, xStart: number, yStart: number, length: number, stepSize: number, horizontal: boolean, color: string, numLines = 50
) => { // Reduced the number of lines
const allLines = [];
for (let i = 0; i < numLines; i++) {
const brownianPath = create_brownian_path(length, stepSize, horizontal);
const pathData = [];
for (let j = 0; j < brownianPath.length; j += 2) {
if (horizontal) {
pathData.push({ x: xStart + brownianPath[j], y: yStart + brownianPath[j + 1] });
} else {
pathData.push({ x: xStart + brownianPath[j + 1], y: yStart + brownianPath[j] });
}
}
const alpha = Math.random();
const line = new fabric.Polyline(pathData, {
stroke: color,
strokeWidth: 1,
fill: 'rgba(0,0,0,0)',
opacity: alpha,
selectable: false,
hasControls: false,
});
allLines.push(line);
}
canvas.add(...allLines);
};
// Batch add objects to the canvas
const objectsToAdd: fabric.Object[] = [];
// Calculate top positions for each color block
let currentTop = padding;
colors.forEach((block) => {
const blockHeight = (height - 4 * padding) * block.height;
const rect = new fabric.Rect({
left: innerPadding,
top: currentTop,
width: width - 2 * innerPadding,
height: blockHeight,
fill: block.color,
selectable: false,
hasControls: false,
rx: 20, // Add border radius for rounded corners
ry: 20, // Add border radius for rounded corners
});
objectsToAdd.push(rect);
for (let i = 0; i < 50; i++) {
const outline = new fabric.Rect({
left: innerPadding - i,
top: currentTop - i,
width: width - 2 * innerPadding + 2 * i,
height: blockHeight + 2 * i,
fill: 'rgba(0, 0, 0, 0)',
stroke: block.color,
strokeWidth: 1,
opacity: (1 - i * 0.02) * 0.7, // Tinge the alpha and color
selectable: false,
hasControls: false,
rx: 20 + i, // Add border radius for rounded corners
ry: 20 + i, // Add border radius for rounded corners
shadow: new fabric.Shadow({
color: `rgba(${parseInt(block.color.slice(1, 3), 16)}, ${parseInt(block.color.slice(3, 5), 16)}, ${parseInt(block.color.slice(5, 7), 16)}, 0.2)`,
blur: 20,
offsetX: 0,
offsetY: 0,
affectStroke: true,
includeDefaultValues: true,
nonScaling: true,
id: i,
}),
});
canvas.add(outline);
}
// Draw Brownian motion lines for the edges
const darkerColor = fabric.Color.fromHex(block.color).setAlpha(0.8).toRgba();
addBrownianLines(canvas, innerPadding, currentTop, width - innerPadding, brownianIntensity, true, darkerColor);
addBrownianLines(canvas, innerPadding, currentTop + blockHeight, width - innerPadding, brownianIntensity, true, darkerColor);
addBrownianLines(canvas, innerPadding, currentTop, blockHeight, brownianIntensity, false, darkerColor);
addBrownianLines(canvas, width - innerPadding, currentTop, blockHeight, brownianIntensity, false, darkerColor);
currentTop += blockHeight + padding;
});
// Batch add all objects to the canvas
canvas.add(...objectsToAdd);
canvas.renderAll(); // Render all changes
}
}
};
const debouncedHandleResize = debounce(handleResize, 300);
window.addEventListener('resize', debouncedHandleResize);
debouncedHandleResize(); // Initial call to set the canvas size correctly
return () => window.removeEventListener('resize', debouncedHandleResize);
}, [editor, colors, brownianIntensity, padding, innerPadding]);
// Function to handle color change
const handleColorChange = useCallback(
debounce((index: number, color: string) => {
setColors((prevColors) => {
const newColors = [...prevColors];
newColors[index].color = color;
return newColors;
});
}, 300),
[]
);
// Function to handle height change
const handleHeightChange = useCallback(
debounce((index: number, height: number) => {
setColors((prevColors) => {
const newColors = [...prevColors];
newColors[index].height = height;
return newColors;
});
}, 300),
[]
);
// Debounced handlers for number inputs
const handleBrownianIntensityChange = useCallback(
debounce((value: number | string) => setBrownianIntensity(Number(value)), 300),
[]
);
const handlePaddingChange = useCallback(
debounce((value: number | string) => setPadding(Number(value)), 300),
[]
);
const handleInnerPaddingChange = useCallback(
debounce((value: number | string) => setInnerPadding(Number(value)), 300),
[]
);
return (
<>
<div ref={containerRef} style={{ width: '100%', height: '1000px' }}>
<FabricJSCanvas className="canvas" onReady={onReady} />
</div>
<Space h={100} />
<TextSpoiler title={<Title>Roll Your Own</Title>}>
<Space h={50} />
<Title order={2} style={{ margin: '10px 0px 50px 0px' }}>Choose Colors</Title>
<div style={{ display: 'flex', justifyContent: 'space-around', marginBottom: '20px' }}>
{colors.map((color, index) => (
<div key={index}>
<ColorPicker
format="hex"
value={color.color}
onChange={(value: string) => handleColorChange(index, value)}
size="xs"
swatches={['#2e2e2e', '#868e96', '#fa5252', '#e64980', '#be4bdb', '#7950f2', '#4c6ef5', '#228be6', '#15aabf', '#12b886', '#40c057', '#82c91e', '#fab005', '#fd7e14']}
/>
<Text>{color.color}</Text>
</div>
))}
</div>
<Title order={2} style={{ margin: '10px 0px 10px 0px' }}>Choose Heights</Title>
<div style={{ display: 'flex', justifyContent: 'space-around', marginBottom: '20px' }}>
{colors.map((color, index) => (
<div key={index}>
<NumberInput
value={color.height}
onChange={(value) => handleHeightChange(index, Number(value))}
min={0}
max={1}
step={0.01}
label={`Height for Block ${index + 1}`}
description={`Adjust the height proportion for Block ${index + 1}`}
/>
</div>
))}
</div>
<Title order={2} style={{ margin: '10px 0px 10px 0px' }}>Choose Brownian Intensity</Title>
<NumberInput
value={brownianIntensity}
onChange={(value) => handleBrownianIntensityChange(value)}
min={1}
max={20}
label="Brownian Intensity"
description="Select the intensity of the Brownian motion"
/>
<Title order={2} style={{ margin: '10px 0px 10px 0px' }}>Choose Padding Amounts</Title>
<NumberInput
value={padding}
onChange={(value) => handlePaddingChange(value)}
min={10}
max={100}
label="Padding"
description="Select the padding amount"
/>
<NumberInput
value={innerPadding}
onChange={(value) => handleInnerPaddingChange(value)}
min={10}
max={100}
label="Inner Padding"
description="Select the inner padding amount"
/>
</TextSpoiler>
<TextSpoiler title={<Title>View Code</Title>}>
<CodeH language='tsx'>
{`This is the code inside of the code. Look up kines for more fun adventures`}
</CodeH>
</TextSpoiler>
</>
);
};
export default RothkoCanvas;
use wasm_bindgen::prelude::*;
use std::f64::consts::PI;
#[wasm_bindgen]
pub fn create_brownian_path(length: usize, step_size: f64, horizontal: bool) -> Vec<f64> {
let mut path_data = Vec::with_capacity(length * 2);
let mut x = 0.0;
let mut y = 0.0;
for _ in 0..length {
let random_step: f64 = step_size * (rand::random::<f64>() - 0.5);
if horizontal {
x += 1.0;
y += random_step;
} else {
x += random_step;
y += 1.0;
}
if horizontal {
path_data.push(x);
path_data.push(y);
} else {
path_data.push(y);
path_data.push(x);
}
}
path_data
}