Skip to content

Commit

Permalink
implement gradient support
Browse files Browse the repository at this point in the history
  • Loading branch information
emilwidlund committed May 31, 2024
1 parent 28b93df commit 066f7f7
Show file tree
Hide file tree
Showing 15 changed files with 265 additions and 110 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "NodeType" ADD VALUE 'GRADIENT';
3 changes: 2 additions & 1 deletion apps/web/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
}

datasource db {
Expand Down Expand Up @@ -108,6 +108,7 @@ enum NodeType {
FROM_HSL
TO_RGB
FROM_RGB
GRADIENT
IMAGE
WEBCAM
IMAGE_VARIATION_AI
Expand Down
60 changes: 8 additions & 52 deletions apps/web/src/components/Controls/ColorControl/ColorControl.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { Input, Output } from '@bitspace/circuit';
import { observer } from 'mobx-react-lite';
import {
ColorSchema,
HSLSchema,
HSVSchema,
HexSchema,
RGBSchema
} from '@bitspace/schemas';
import {
ChangeEventHandler,
FocusEventHandler,
Expand All @@ -15,16 +8,10 @@ import {
useMemo,
useState
} from 'react';
import { z } from 'zod';
import { hsv2rgb } from '../../ColorPicker/ColorPicker.utils';
import { hex, hsl, rgb } from 'color-convert';
import { hsl, rgb } from 'color-convert';
import clsx from 'clsx';

export type HexColorSchemaType = z.infer<ReturnType<typeof HexSchema>>;
export type RGBColorSchemaType = z.infer<ReturnType<typeof RGBSchema>>;
export type HSLColorSchemaType = z.infer<ReturnType<typeof HSLSchema>>;
export type HSVColorSchemaType = z.infer<ReturnType<typeof HSVSchema>>;
type ColorSchemaType = z.infer<ReturnType<typeof ColorSchema>>;
import { ColorSchemaType, resolveColor } from '@/utils';

export interface ColorControlProps<T extends ColorSchemaType> {
port: Input<T> | Output<T>;
Expand Down Expand Up @@ -67,51 +54,20 @@ export const ColorControl = observer(function <T extends ColorSchemaType>({
return '#000000';
}, [color]);

const resolveColor = useCallback(
(v: string): T => {
if (typeof color === 'object') {
if ('red' in color) {
const [red, green, blue] = hex.rgb(v);

return RGBSchema().parse({
red,
green,
blue
}) as T;
} else if ('hue' in color && 'luminance' in color) {
const [hue, saturation, luminance] = hex.hsl(v);

return HSLSchema().parse({
hue,
saturation: saturation / 100,
luminance: luminance / 100
}) as T;
} else {
const [hue, saturation, value] = hex.hsv(v);

return HSVSchema().parse({
hue,
saturation: saturation / 100,
value: value / 100
}) as T;
}
} else {
return HexSchema().parse(v) as T;
}
},
[color]
);

const handleBlur: FocusEventHandler<HTMLInputElement> = useCallback(
e => {
onBlur?.(resolveColor(e.target.value));
if (color) {
onBlur?.(resolveColor(e.target.value, color));
}
},
[onBlur, color]
);

const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
e => {
port.next(resolveColor(e.target.value));
if (color) {
port.next(resolveColor(e.target.value, color));
}
},
[port, color]
);
Expand Down
51 changes: 51 additions & 0 deletions apps/web/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { z } from 'zod';
import { Position } from './circuit';
import {
ColorSchema,
HSLSchema,
HSVSchema,
HexSchema,
RGBSchema
} from '@bitspace/schemas';
import { hex } from 'color-convert';

export const lerp = (a: number, b: number, t: number) => {
return a * (1 - t) + b * t;
Expand All @@ -7,6 +16,48 @@ export const lerp = (a: number, b: number, t: number) => {
export const distanceBetween = (a: Position, b: Position) => {
return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
};

export const angleBetween = (a: Position, b: Position) => {
return Math.atan2(b.x - a.x, b.y - a.y);
};

export type HexColorSchemaType = z.infer<ReturnType<typeof HexSchema>>;
export type RGBColorSchemaType = z.infer<ReturnType<typeof RGBSchema>>;
export type HSLColorSchemaType = z.infer<ReturnType<typeof HSLSchema>>;
export type HSVColorSchemaType = z.infer<ReturnType<typeof HSVSchema>>;
export type ColorSchemaType = z.infer<ReturnType<typeof ColorSchema>>;

export const resolveColor = <T extends ColorSchemaType>(
raw: string,
color: T
): T => {
if (typeof color === 'object') {
if ('red' in color) {
const [red, green, blue] = hex.rgb(raw);

return RGBSchema().parse({
red,
green,
blue
}) as T;
} else if ('hue' in color && 'luminance' in color) {
const [hue, saturation, luminance] = hex.hsl(raw);

return HSLSchema().parse({
hue,
saturation: saturation / 100,
luminance: luminance / 100
}) as T;
} else {
const [hue, saturation, value] = hex.hsv(raw);

return HSVSchema().parse({
hue,
saturation: saturation / 100,
value: value / 100
}) as T;
}
} else {
return HexSchema().parse(raw) as T;
}
};
58 changes: 58 additions & 0 deletions apps/web/src/windows/GradientWindow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useEffect, useState } from 'react';
import { hsv2rgb } from '../components/ColorPicker/ColorPicker.utils';
import { NodeWindow } from '../circuit/components/Node/Node';
import { FromHSV } from '../../../../packages/nodes/src/color/FromHSV/FromHSV';
import { ColorSchema, GradientSchema } from '@bitspace/schemas';
import { Gradient } from '@bitspace/nodes';

const defaultGradient: Zod.infer<ReturnType<typeof GradientSchema>> = {
type: 'linear',
colors: [
{ color: { hue: 0, saturation: 0, value: 0 }, position: 0 },
{ color: { hue: 0, saturation: 0, value: 0 }, position: 1 }
],
angle: 0
};

export const GradientWindow = ({ node }: { node: Gradient }) => {
const [color, setColor] =
useState<Zod.infer<ReturnType<typeof GradientSchema>>>(defaultGradient);

useEffect(() => {
const subscription = node.outputs.output.subscribe(value => {
setColor(value as Zod.infer<ReturnType<typeof GradientSchema>>);
});

return () => {
subscription.unsubscribe();
};
}, [node]);

const gradient = [
'linear-gradient(',
`${color.angle}deg, `,
`${color.colors
.map(({ color }) => {
const [r, g, b] = hsv2rgb(
color.hue,
color.saturation,
color.value
);
return `rgb(${r}, ${g}, ${b})`;
})
.join(', ')}`,
')'
].join('');

return (
<NodeWindow className="bg-transparent">
<div
className="w-full h-[226px] rounded-full"
style={{
background: gradient
}}
/>
</NodeWindow>
);
};
4 changes: 3 additions & 1 deletion apps/web/src/windows/HSVWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ export const HSVWindow = ({ node }: { node: FromHSV }) => {
const [rgb, setRgb] = useState<[number, number, number]>([0, 0, 0]);

useEffect(() => {
node.inputs.color.subscribe(hsv => {
const subscription = node.inputs.color.subscribe(hsv => {
const rgb = hsv2rgb(hsv.hue, hsv.saturation, hsv.value);
setRgb(rgb);
});

return () => subscription.unsubscribe();
}, [node]);

const [r, g, b] = rgb;
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/windows/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import { MeshWindow } from './MeshWindow';
import { Mesh } from '../../../../packages/nodes/src/3d/Mesh/Mesh';
import { ImageEditWindow } from './ImageEditWindow';
import { ImageEdit } from '../../../../packages/nodes/src/ai/ImageEdit/ImageEdit';
import { Image, Oscillator } from '@bitspace/nodes';
import { Gradient, Image, Oscillator } from '@bitspace/nodes';
import { Webcam } from '@bitspace/nodes';
import { WebcamWindow } from './WebcamWindow';
import { OscillatorWindow } from './OscillatorWindow';
import { GradientWindow } from './GradientWindow';

export const nodeWindowResolver: NodeWindowResolver = (node: Node) => {
if (!('displayName' in node.constructor)) return <></>;
Expand Down Expand Up @@ -62,5 +63,7 @@ export const nodeWindowResolver: NodeWindowResolver = (node: Node) => {
);
case 'From HSV':
return <HSVWindow node={node as FromHSV} />;
case 'Gradient':
return <GradientWindow node={node as Gradient} />;
}
};
56 changes: 56 additions & 0 deletions packages/nodes/src/color/Gradient/Gradient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { NodeType } from '../../types';
import { Input, Node, Output } from '@bitspace/circuit';
import { ColorSchema, GradientSchema, minMaxNumber } from '@bitspace/schemas';
import { combineLatest, map } from 'rxjs';

export class Gradient extends Node {
static displayName = 'Gradient';
static type = NodeType.GRADIENT;

inputs = {
a: new Input({
name: 'A',
type: ColorSchema(),
defaultValue: {
red: 0,
green: 0,
blue: 0
}
}),
b: new Input({
name: 'B',
type: ColorSchema(),
defaultValue: {
red: 0,
green: 0,
blue: 0
}
}),
angle: new Input({
name: 'Angle',
type: minMaxNumber(0, 360, true),
defaultValue: 0
})
};

outputs = {
output: new Output({
name: 'Output',
type: GradientSchema(),
observable: combineLatest([
this.inputs.a,
this.inputs.b,
this.inputs.angle
]).pipe(
map(([a, b, angle]) => ({
type: 'linear',
colors: [
{ color: a, position: 0 },
{ color: b, position: 1 }
],
angle
}))
)
})
};
}
10 changes: 7 additions & 3 deletions packages/nodes/src/color/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AnalogousHarmony } from './AnalogousHarmony/AnalogousHarmony';
import { ComplementaryHarmony } from './ComplementaryHarmony/ComplementaryHarmony';
import { FromHSV } from './FromHSV/FromHSV';
import { FromRGB } from './FromRGB/FromRGB';
import { Gradient } from './Gradient/Gradient';
import { SquareHarmony } from './SquareHarmony/SquareHarmony';
import { TetradicHarmony } from './TetradicHarmony/TetradicHarmony';
import { ToHSV } from './ToHSV/ToHSV';
Expand All @@ -16,7 +17,8 @@ export const ColorNodes = [
ComplementaryHarmony,
FromHSV,
ToHSV,
FromRGB
FromRGB,
Gradient
].sort((a, b) => a.displayName.localeCompare(b.displayName));

export type ColorNode =
Expand All @@ -27,7 +29,8 @@ export type ColorNode =
| ComplementaryHarmony
| FromHSV
| ToHSV
| FromRGB;
| FromRGB
| Gradient;

export interface ColorNodeConstructor {
new (): ColorNode;
Expand All @@ -42,5 +45,6 @@ export {
ComplementaryHarmony,
FromHSV,
ToHSV,
FromRGB
FromRGB,
Gradient
};
1 change: 1 addition & 0 deletions packages/nodes/src/descriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const NodeDescriptionsMap: Record<NodeType, string> = {
[NodeType.TO_RGB]: 'Returns a RGB color from a given red, green & blue',
[NodeType.TO_HSV]:
'Returns a HSV color from a given hue, saturation & value',
[NodeType.GRADIENT]: 'Returns a gradient based on the input colors',

[NodeType.IMAGE]: 'Returns an image from the given source URL',
[NodeType.WEBCAM]: 'Returns a webcam stream',
Expand Down
1 change: 1 addition & 0 deletions packages/nodes/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export enum NodeType {
FROM_HSL = 'FROM_HSL',
TO_RGB = 'TO_RGB',
FROM_RGB = 'FROM_RGB',
GRADIENT = 'GRADIENT',
IMAGE = 'IMAGE',
WEBCAM = 'WEBCAM',
IMAGE_VARIATION_AI = 'IMAGE_VARIATION_AI',
Expand Down
Loading

0 comments on commit 066f7f7

Please sign in to comment.