Skip to content

Commit

Permalink
WIP: Color properties (#26)
Browse files Browse the repository at this point in the history
* work on color properties

* performance optimizations

* hide timeline for color properties

* reduce node editor t value decimal places

* color input node decimal places

* remove return statement

* mild refactor

Co-authored-by: alexharri <alex@taktikal.is>
  • Loading branch information
alexharri and alexharri committed Jul 12, 2020
1 parent 0a3e2fb commit 3fad4e4
Show file tree
Hide file tree
Showing 35 changed files with 1,505 additions and 118 deletions.
246 changes: 246 additions & 0 deletions src/components/colorPicker/ColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import React, { useRef, useState, useLayoutEffect } from "react";
import { compileStylesheetLabelled } from "~/util/stylesheets";
import { RGBColor } from "~/types";
import { rgbToHSL, hslToRGB } from "~/util/color/convertColor";
import { useCanvasPixelSelector } from "~/hook/useCanvasPixelSelector";
import { useDidUpdate } from "~/hook/useDidUpdate";

const s = compileStylesheetLabelled(({ css }) => ({
container: css`
display: flex;
`,

colorCursor: css`
position: absolute;
background: white;
width: 12px;
height: 12px;
border: 2px solid white;
background: black;
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
`,

hueCursor: css`
position: absolute;
background: white;
left: -4px;
right: -4px;
height: 2px;
transform: translateY(-50%);
pointer-events: none;
`,
}));

const WIDTH = 256;
const HEIGHT = WIDTH;
const STRIP_WIDTH = 16;

function renderBlock(ctx: CanvasRenderingContext2D, hue: number) {
const color = hslToRGB([hue, 100, 50]);

ctx.fillStyle = `rgb(${color.join(",")})`;
ctx.fillRect(0, 0, WIDTH, HEIGHT);

const white = ctx.createLinearGradient(0, 0, WIDTH, 0);
white.addColorStop(0, "rgba(255,255,255,1)");
white.addColorStop(1, "rgba(255,255,255,0)");

const black = ctx.createLinearGradient(0, 0, 0, HEIGHT);
black.addColorStop(0, "rgba(0,0,0,0)");
black.addColorStop(1, "rgba(0,0,0,1)");

ctx.fillStyle = white;
ctx.fillRect(0, 0, WIDTH, HEIGHT);

ctx.fillStyle = black;
ctx.fillRect(0, 0, WIDTH, HEIGHT);

// Ensure that corners are not diluted in any way
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, 1, 1);

ctx.fillStyle = `rgb(${color.join(",")})`;
ctx.fillRect(WIDTH - 1, 0, 1, 1);
}

const Strip: React.FC<{ hue: number; onHueChange: (hue: number) => void }> = (props) => {
const canvas = useRef<HTMLCanvasElement>(null);
const ctx = useRef<CanvasRenderingContext2D | null>(null);
const [y, setY] = useState(() => Math.round((props.hue / 360) * HEIGHT));

useLayoutEffect(() => {
ctx.current = canvas.current?.getContext("2d") || null;
}, [canvas.current]);

// Render strip once on mount
useLayoutEffect(() => {
if (!ctx.current) {
return;
}

for (let i = 0; i < HEIGHT; i += 1) {
ctx.current.fillStyle = `hsl(${(i / HEIGHT) * 360}, 100%, 50%)`;
ctx.current.fillRect(0, i, STRIP_WIDTH, 1);
}
}, [ctx.current]);

const pixelSelector = useCanvasPixelSelector(
canvas,
{ allowOutside: true },
(rgbColor, position) => {
const [hue] = rgbToHSL(rgbColor);
props.onHueChange(hue);
setY(position.y);
},
);

return (
<div style={{ position: "relative", marginRight: 16 }}>
<canvas ref={canvas} height={HEIGHT} width={STRIP_WIDTH} {...pixelSelector} />
<div style={{ top: y }} className={s("hueCursor")} />
</div>
);
};

/**
* Returns the position of the RGB color in the context.
*
* If an exact color match is not found, the position of the
* closest color is returned.
*/
const findPositionOfColor = (ctx: CanvasRenderingContext2D, rgbColor: RGBColor): Vec2 => {
const h = ctx.canvas.height;
const w = ctx.canvas.width;

let dist = Infinity;
let closestPos: Vec2 = Vec2.new(0, 0);

const imageData = ctx.getImageData(0, 0, w, h).data;

for (let i = 0; i < w; i += 1) {
for (let j = 0; j < h; j += 1) {
const r = imageData[j * w * 4 + i * 4];
const g = imageData[j * w * 4 + i * 4 + 1];
const b = imageData[j * w * 4 + i * 4 + 2];

const currColor: RGBColor = [r, g, b];
const currDist = currColor.reduce((acc, _, i) => {
return acc + Math.abs(currColor[i] - rgbColor[i]);
}, 0);

if (currDist === 0) {
return Vec2.new(i, j);
}

if (currDist < dist) {
dist = currDist;
closestPos = Vec2.new(i, j);
}
}
}

return closestPos;
};

const Block: React.FC<{ rgb: RGBColor; hue: number; onRgbChange: (rgb: RGBColor) => void }> = (
props,
) => {
const canvas = useRef<HTMLCanvasElement>(null);
const ctx = useRef<CanvasRenderingContext2D | null>(null);
const [position, setPosition] = useState<Vec2 | null>(null);

useLayoutEffect(() => {
ctx.current = canvas.current?.getContext("2d") || null;
}, [canvas.current]);

useLayoutEffect(() => {
if (!ctx.current) {
return;
}

renderBlock(ctx.current, props.hue);

// Get initial position of color cursor
if (!position) {
const _ctx = ctx.current;
setPosition(findPositionOfColor(_ctx, props.rgb));
}
}, [ctx.current, props.hue]);

const pixelSelector = useCanvasPixelSelector(
canvas,
{ allowOutside: true, shiftPosition: Vec2.new(0, -5) },
(rgbColor, position) => {
props.onRgbChange(rgbColor);
setPosition(position);
},
);

return (
<div style={{ position: "relative" }}>
<canvas
ref={canvas}
height={HEIGHT}
width={WIDTH}
style={{ marginRight: 16 }}
{...pixelSelector}
/>
{position && (
<div
className={s("colorCursor")}
style={{
left: position.x,
top: position.y,
background: `rgb(${props.rgb.join(",")})`,
}}
/>
)}
</div>
);
};

interface Props {
rgbColor: RGBColor;
onChange: (rgbColor: RGBColor) => void;
}

export const ColorPicker: React.FC<Props> = (props) => {
const [initialRgb] = useState(props.rgbColor);
const [rgb, setRgb] = useState<RGBColor>(props.rgbColor);
const [hue, setHue] = useState<number>(() => rgbToHSL(rgb)[0]);

// Update selected color on hue change
useDidUpdate(() => {
const [, s, l] = rgbToHSL(rgb);
setRgb(hslToRGB([hue, s, l]));
}, [hue]);

useDidUpdate(() => {
props.onChange(rgb);
}, [rgb]);

return (
<div className={s("container")}>
<Block hue={hue} onRgbChange={setRgb} rgb={rgb} />
<Strip hue={hue} onHueChange={setHue} />
<div>
<div
style={{
height: 32,
width: 64,
background: `rgb(${rgb.join(",")})`,
}}
/>
<div
style={{
height: 32,
width: 64,
background: `rgb(${initialRgb.join(",")})`,
}}
/>
</div>
</div>
);
};
35 changes: 31 additions & 4 deletions src/composition/compositionTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ValueType, PropertyName, PropertyGroupName } from "~/types";
import { ValueType, PropertyName, PropertyGroupName, ValueFormat, RGBColor } from "~/types";

export interface Composition {
id: string;
Expand Down Expand Up @@ -28,16 +28,43 @@ export interface CompositionPropertyGroup {
collapsed: boolean;
}

export interface CompositionProperty {
export type CompositionProperty = {
type: "property";
id: string;
layerId: string;
compositionId: string;
name: PropertyName;
valueType: ValueType;
value: number;
valueFormat?: ValueFormat;
timelineId: string;
color?: string;
min?: number;
max?: number;
} & (
| {
valueType: ValueType.Any;
value: any;
}
| {
valueType: ValueType.Color;
value: RGBColor;
}
| {
valueType: ValueType.Number;
value: number;
}
| {
valueType: ValueType.Rect;
value: Rect;
}
| {
valueType: ValueType.Vec2;
value: Vec2;
}
);

export interface PropertyToValueMap {
[propertyId: string]: {
rawValue: unknown;
computedValue: unknown;
};
}
5 changes: 3 additions & 2 deletions src/composition/state/compositionReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
modifyItemInMap,
modifyItemInUnionMap,
} from "~/util/mapUtils";
import { RGBAColor } from "~/types";

const createLayerId = (layers: CompositionState["layers"]) =>
(
Expand Down Expand Up @@ -102,7 +103,7 @@ export const compositionActions = {
}),

setPropertyValue: createAction("comp/SET_PROPERTY_VALUE", (action) => {
return (propertyId: string, value: number) => action({ propertyId, value });
return (propertyId: string, value: number | RGBAColor) => action({ propertyId, value });
}),

setPropertyGroupCollapsed: createAction("comp/SET_PROP_GROUP_COLLAPSED", (action) => {
Expand Down Expand Up @@ -174,7 +175,7 @@ export const compositionReducer = (
propertyId,
(item: CompositionProperty) => ({
...item,
value,
value: value as any,
}),
),
};
Expand Down
2 changes: 1 addition & 1 deletion src/composition/timeline/CompositionTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Composition, CompositionProperty } from "~/composition/compositionTypes
import { splitRect, capToRange } from "~/util/math";
import { RequestActionCallback, requestAction } from "~/listener/requestAction";
import { separateLeftRightMouse } from "~/util/mouse";
import { CompTimeLayer } from "~/composition/timeline/CompTimeLayer";
import { CompTimeLayer } from "~/composition/timeline/layer/CompTimeLayer";
import { ViewBounds } from "~/timeline/ViewBounds";
import { areaActions } from "~/area/state/areaActions";
import { useKeyDownEffect } from "~/hook/useKeyDown";
Expand Down
4 changes: 4 additions & 0 deletions src/composition/timeline/compTimeContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import React from "react";
import { PropertyToValueMap } from "~/composition/compositionTypes";

export const CompTimePropertyValueContext = React.createContext<PropertyToValueMap>({});
Loading

0 comments on commit 3fad4e4

Please sign in to comment.