Skip to content

Commit

Permalink
Very basic concept
Browse files Browse the repository at this point in the history
  • Loading branch information
ucarion committed Dec 19, 2019
0 parents commit 39e3cb7
Show file tree
Hide file tree
Showing 10 changed files with 4,820 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"dependencies": {
"@types/react": "^16.9.16",
"@types/react-dom": "^16.9.4",
"html-webpack-plugin": "^3.2.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"ts-loader": "^6.2.1",
"typescript": "^3.7.3",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0"
}
}
216 changes: 216 additions & 0 deletions src/App/TunnelGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import React, { useMemo } from "react";
import {
MIN_SRGB_LUMINANCE,
MAX_SRGB_LUMINANCE,
MIN_SRGB_CHROMA,
MAX_SRGB_CHROMA,
MIN_SRGB_HUE,
MAX_SRGB_HUE,
lchToRGB,
rgbIsDisplayable,
rgbToLCH,
LCH,
RGB
} from "../color";

interface Props {
axis: "l" | "c" | "h";
sequence: RGB[];
}

export default function TunnelGraph({ axis, sequence }: Props) {
const pathCommand = useMemo(() => {
const ranges = sequence.map(color => sweepRange(color, axis));
const sequenceRangeEdges = ranges.map((range, index) => ({
sequenceNumber: index,
edges: findRangeEdges(range)
}));

const coverPaths = findCoverPaths(sequenceRangeEdges);

return coverPaths
.map(
path =>
path
.map(
([i, j], index) =>
`${index === 0 ? "M" : "L"}${i / (sequence.length - 1)} ${j /
(SWEEP_STEPS - 1)}`
)
.join(" ") + "Z"
)
.join(" ");
}, [axis, sequence]);

return (
<svg width="100%" height="100%" preserveAspectRatio="none">
<defs>
<pattern
id="background"
width="6"
height="6"
patternUnits="userSpaceOnUse"
preserveAspectRatio="none"
>
<path
d="M0 0 L1 0 L6 5 L6 6 L5 6 L0 1 Z M5 0 L6 0 L6 1 Z M0 5 L0 6 L1 6 Z"
fill="#aaa"
/>
</pattern>
</defs>

<rect fill="url(#background)" width="100%" height="100%" />

<svg
width="100%"
height="100%"
viewBox="0 0 1 1"
preserveAspectRatio="none"
>
<path fillRule="evenodd" fill="green" d={pathCommand} />
</svg>
</svg>
);
}

const SWEEP_STEPS = 100;

function sweepRange(rgb: RGB, axis: "l" | "c" | "h"): boolean[] {
const lch = rgbToLCH(rgb);

const range = [];
for (let i = 0; i < SWEEP_STEPS; i++) {
const { [axis]: _, ...rest } = lch;
const [min, max] = {
l: [MIN_SRGB_LUMINANCE, MAX_SRGB_LUMINANCE],
c: [MIN_SRGB_CHROMA, MAX_SRGB_CHROMA],
h: [MIN_SRGB_HUE, MAX_SRGB_HUE]
}[axis];

const result = {
[axis]: min + (max * i) / (SWEEP_STEPS - 1),
...rest
} as LCH;
range.push(rgbIsDisplayable(lchToRGB(result)));
}

return range;
}

interface RangeEdges {
risingEdges: number[];
fallingEdges: number[];
}

interface SequenceRangeEdges {
sequenceNumber: number;
edges: RangeEdges;
}

function findRangeEdges(vals: boolean[]): RangeEdges {
const risingEdges = [];

if (vals[0]) {
risingEdges.push(0);
}

for (let i = 1; i < vals.length; i++) {
if (!vals[i - 1] && vals[i]) {
risingEdges.push(i);
}
}

const fallingEdges = [];

for (let i = 0; i < vals.length - 1; i++) {
if (vals[i] && !vals[i + 1]) {
fallingEdges.push(i);
}
}

if (vals[vals.length - 1]) {
fallingEdges.push(vals.length - 1);
}

return { risingEdges, fallingEdges };
}

function findCoverPaths(sequence: SequenceRangeEdges[]): [number, number][][] {
let result: [number, number][][] = [];
let subSequences = [sequence];
let positive = true;

while (subSequences.length !== 0) {
for (const subSequence of subSequences) {
if (positive) {
result.push(positivePass(subSequence));
} else {
result.push(negativePass(subSequence));
}
}

subSequences = ([] as SequenceRangeEdges[][]).concat(
...subSequences.map(subSequence => splitIntervals(subSequence))
);

positive = !positive;
}

return result;
}

function positivePass(sequence: SequenceRangeEdges[]): [number, number][] {
const result = [];

for (let i = 0; i < sequence.length; i++) {
result.push([
sequence[i].sequenceNumber,
sequence[i].edges.risingEdges.shift()
] as [number, number]);
}

for (let i = sequence.length - 1; i >= 0; i--) {
result.push([
sequence[i].sequenceNumber,
sequence[i].edges.fallingEdges.pop()
] as [number, number]);
}

return result;
}

function negativePass(sequence: SequenceRangeEdges[]) {
const result = [];

for (let i = 0; i < sequence.length; i++) {
result.push([
sequence[i].sequenceNumber,
sequence[i].edges.fallingEdges.pop()
] as [number, number]);
}

for (let i = sequence.length - 1; i >= 0; i--) {
result.push([
sequence[i].sequenceNumber,
sequence[i].edges.risingEdges.shift()
] as [number, number]);
}

return result;
}

function splitIntervals(
sequence: SequenceRangeEdges[]
): SequenceRangeEdges[][] {
const result: SequenceRangeEdges[][] = [[]];

for (const sequenceRange of sequence) {
if (sequenceRange.edges.risingEdges.length === 0) {
result.push([]);
} else {
result[result.length - 1].push(sequenceRange);
}
}

return result.filter(subSequence => subSequence.length !== 0);
}
121 changes: 121 additions & 0 deletions src/App/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React, { useState, ChangeEvent } from "react";
import { RGB } from "../color";
import TunnelGraph from "./TunnelGraph";
import { Palette } from "./types";

const DEFAULT_COLOR = { r: 0.5, g: 0.5, b: 0.5 };

export default function App() {
const [{ hues, shades, colors }, setPalette] = useState<Palette>({
hues: ["hue"],
shades: ["shade"],
colors: [[DEFAULT_COLOR]]
});

const [selectedColor, setSelectedColor] = useState({ hue: 0, shade: 0 });

const channelToHex = (t: number): string => {
return Math.round(t * 255)
.toString(16)
.padStart(2, "0");
};

const rgbToHex = ({ r, g, b }: RGB): string => {
return `#${channelToHex(r)}${channelToHex(g)}${channelToHex(b)}`;
};

const hexToRGB = (hex: string): RGB => {
return {
r: parseInt(hex.substring(1, 3), 16) / 255,
g: parseInt(hex.substring(3, 5), 16) / 255,
b: parseInt(hex.substring(5, 7), 16) / 255
};
};

const handleAddHue = () => {
setPalette({
hues: [...hues, "new hue"],
shades,
colors: [...colors, Array(shades.length).fill(DEFAULT_COLOR)]
});
};

const handleAddShade = () => {
setPalette({
hues,
shades: [...shades, "new shade"],
colors: colors.map(sequence => [...sequence, DEFAULT_COLOR])
});
};

const handleColorChange = (event: ChangeEvent<HTMLInputElement>) => {
const color = hexToRGB(event.target.value);
const { hue, shade } = selectedColor;

setPalette({
hues,
shades,
colors: [
...colors.slice(0, hue),
[
...colors[hue].slice(0, shade),
color,
...colors[hue].slice(shade + 1)
],
...colors.slice(hue + 1)
]
});
};

const hueSequence = colors[selectedColor.hue];
const shadeSequence = colors.map(sequence => sequence[selectedColor.shade]);

return (
<div style={{ display: "grid", gridTemplateColumns: "500px auto" }}>
<div>
<div
style={{
display: "grid",
gridTemplateColumns: `repeat(${shades.length}, 1fr)`,
gridTemplateRows: `repeat(${hues.length}, 50px)`
}}
>
{colors.map((sequence, hue) =>
sequence.map((color, shade) => (
<div
key={`${hue}.${shade}`}
style={{ backgroundColor: rgbToHex(color) }}
onClick={() => setSelectedColor({ hue, shade })}
>
{rgbToHex(color)}
</div>
))
)}
</div>

<input
type="color"
value={rgbToHex(colors[selectedColor.hue][selectedColor.shade])}
onChange={handleColorChange}
/>

<button onClick={handleAddHue}>Add Hue</button>
<button onClick={handleAddShade}>Add Shade</button>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: `repeat(2, 1fr)`,
gridTemplateRows: `repeat(3, 1fr)`
}}
>
<TunnelGraph axis="l" sequence={hueSequence} />
<TunnelGraph axis="l" sequence={shadeSequence} />
<TunnelGraph axis="c" sequence={hueSequence} />
<TunnelGraph axis="c" sequence={shadeSequence} />
<TunnelGraph axis="h" sequence={hueSequence} />
<TunnelGraph axis="h" sequence={shadeSequence} />
</div>
</div>
);
}
7 changes: 7 additions & 0 deletions src/App/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { RGB } from "../color";

export interface Palette {
hues: string[];
shades: string[];
colors: RGB[][]; // invariant: colors.length === hues.length, and colors[i].length === shades.length
}
Loading

0 comments on commit 39e3cb7

Please sign in to comment.