Curves image adjustment node (#1214)
* Create ValueMapperNode and use it for brightness/contrast * move spline code into seperate module * Add GenerateCurvesNode * add a `LuminanceMut`-trait * add `lerp` to `Channel` * Add frontend code to handle the curves widget's inputs * Rename spline module to curve * Make messages in CurveInput pass * Improve curves widget design and fix sizing issue * Implement proper bezier handling * Use bezier_rs's intersections function instead of own cubic root solver * Debounce CurveInput events and change how debouncer works the first event issued to the debouncer was unneccessarily delayed. Instead now the debouncer fires it instantaneously but blocks events that come in until a timeout was reached. * Make curve editing more user friendly * Change code to use project terminology * sample -> manipulator group or manipulator * marker -> handle * Fix small documentation mistake in bezier-rs * Add find_tvalues_for_x function to bezier-rs also integrate the function into curves node * Add tests for find_tvalues_for_x in bezier-rs * Fix formatting * Revert BrightnessContrastNode changes * Frontend cleanup --------- Co-authored-by: Keavon Chambers <keavon@keavon.com> Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
parent
cfe38c6413
commit
dc4b16aead
|
|
@ -123,6 +123,12 @@ impl<F: Fn(&MessageDiscriminant) -> Vec<KeysGroup>> MessageHandler<LayoutMessage
|
||||||
let callback_message = (color_input.on_update.callback)(color_input);
|
let callback_message = (color_input.on_update.callback)(color_input);
|
||||||
responses.add(callback_message);
|
responses.add(callback_message);
|
||||||
}
|
}
|
||||||
|
Widget::CurveInput(curve_input) => {
|
||||||
|
let curve = serde_json::from_value(value).expect("CurveInput event data could not be deserialized");
|
||||||
|
curve_input.value = curve;
|
||||||
|
let callback_message = (curve_input.on_update.callback)(curve_input);
|
||||||
|
responses.add(callback_message);
|
||||||
|
}
|
||||||
Widget::DropdownInput(dropdown_input) => {
|
Widget::DropdownInput(dropdown_input) => {
|
||||||
let update_value = value.as_u64().expect("DropdownInput update was not of type: u64");
|
let update_value = value.as_u64().expect("DropdownInput update was not of type: u64");
|
||||||
dropdown_input.selected_index = Some(update_value as u32);
|
dropdown_input.selected_index = Some(update_value as u32);
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,7 @@ impl LayoutGroup {
|
||||||
let val = match &mut widget.widget {
|
let val = match &mut widget.widget {
|
||||||
Widget::CheckboxInput(x) => &mut x.tooltip,
|
Widget::CheckboxInput(x) => &mut x.tooltip,
|
||||||
Widget::ColorInput(x) => &mut x.tooltip,
|
Widget::ColorInput(x) => &mut x.tooltip,
|
||||||
|
Widget::CurveInput(x) => &mut x.tooltip,
|
||||||
Widget::DropdownInput(x) => &mut x.tooltip,
|
Widget::DropdownInput(x) => &mut x.tooltip,
|
||||||
Widget::FontInput(x) => &mut x.tooltip,
|
Widget::FontInput(x) => &mut x.tooltip,
|
||||||
Widget::IconButton(x) => &mut x.tooltip,
|
Widget::IconButton(x) => &mut x.tooltip,
|
||||||
|
|
@ -430,6 +431,7 @@ pub enum Widget {
|
||||||
BreadcrumbTrailButtons(BreadcrumbTrailButtons),
|
BreadcrumbTrailButtons(BreadcrumbTrailButtons),
|
||||||
CheckboxInput(CheckboxInput),
|
CheckboxInput(CheckboxInput),
|
||||||
ColorInput(ColorInput),
|
ColorInput(ColorInput),
|
||||||
|
CurveInput(CurveInput),
|
||||||
DropdownInput(DropdownInput),
|
DropdownInput(DropdownInput),
|
||||||
FontInput(FontInput),
|
FontInput(FontInput),
|
||||||
IconButton(IconButton),
|
IconButton(IconButton),
|
||||||
|
|
@ -512,6 +514,7 @@ impl DiffUpdate {
|
||||||
Widget::PopoverButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
Widget::PopoverButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||||
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||||
Widget::IconLabel(_)
|
Widget::IconLabel(_)
|
||||||
|
| Widget::CurveInput(_)
|
||||||
| Widget::InvisibleStandinInput(_)
|
| Widget::InvisibleStandinInput(_)
|
||||||
| Widget::PivotAssist(_)
|
| Widget::PivotAssist(_)
|
||||||
| Widget::RadioInput(_)
|
| Widget::RadioInput(_)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use document_legacy::layers::layer_info::LayerDataTypeDiscriminant;
|
use document_legacy::layers::layer_info::LayerDataTypeDiscriminant;
|
||||||
use document_legacy::LayerId;
|
use document_legacy::LayerId;
|
||||||
use graphene_core::raster::color::Color;
|
use graphene_core::raster::color::Color;
|
||||||
|
use graphene_core::raster::curve::Curve;
|
||||||
use graphite_proc_macros::WidgetBuilder;
|
use graphite_proc_macros::WidgetBuilder;
|
||||||
|
|
||||||
use derivative::*;
|
use derivative::*;
|
||||||
|
|
@ -414,3 +415,19 @@ pub struct TextInput {
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
pub on_update: WidgetCallback<TextInput>,
|
pub on_update: WidgetCallback<TextInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Derivative, WidgetBuilder, specta::Type)]
|
||||||
|
#[derivative(Debug, PartialEq, Default)]
|
||||||
|
pub struct CurveInput {
|
||||||
|
#[widget_builder(constructor)]
|
||||||
|
pub value: Curve,
|
||||||
|
|
||||||
|
pub disabled: bool,
|
||||||
|
|
||||||
|
pub tooltip: String,
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
#[serde(skip)]
|
||||||
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
|
pub on_update: WidgetCallback<CurveInput>,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
use crate::messages::portfolio::document::node_graph::VectorDataModification;
|
use crate::messages::portfolio::document::node_graph::VectorDataModification;
|
||||||
|
|
||||||
use bezier_rs::{ManipulatorGroup, Subpath};
|
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||||
use document_legacy::document::Document;
|
use document_legacy::document::Document;
|
||||||
use glam::{DAffine2, DVec2};
|
|
||||||
use graph_craft::document::{value::TaggedValue, NodeInput};
|
use graph_craft::document::{value::TaggedValue, NodeInput};
|
||||||
use graphene_core::uuid::ManipulatorGroupId;
|
use graphene_core::uuid::ManipulatorGroupId;
|
||||||
use graphene_core::vector::{ManipulatorPointId, SelectedType};
|
use graphene_core::vector::{ManipulatorPointId, SelectedType};
|
||||||
|
|
||||||
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
||||||
/// Convert an affine transform into the tuple `(scale, angle, translation, shear)` assuming `shear.y = 0`.
|
/// Convert an affine transform into the tuple `(scale, angle, translation, shear)` assuming `shear.y = 0`.
|
||||||
pub fn compute_scale_angle_translation_shear(transform: DAffine2) -> (DVec2, f64, DVec2, DVec2) {
|
pub fn compute_scale_angle_translation_shear(transform: DAffine2) -> (DVec2, f64, DVec2, DVec2) {
|
||||||
let x_axis = transform.matrix2.x_axis;
|
let x_axis = transform.matrix2.x_axis;
|
||||||
|
|
|
||||||
|
|
@ -1524,6 +1524,18 @@ fn static_nodes() -> Vec<DocumentNodeType> {
|
||||||
properties: node_properties::brightness_contrast_properties,
|
properties: node_properties::brightness_contrast_properties,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
DocumentNodeType {
|
||||||
|
name: "Curves",
|
||||||
|
category: "Image Adjustments",
|
||||||
|
identifier: NodeImplementation::proto("graphene_core::raster::CurvesNode<_>"),
|
||||||
|
inputs: vec![
|
||||||
|
DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true),
|
||||||
|
DocumentInputType::value("Curve", TaggedValue::Curve(Default::default()), false),
|
||||||
|
],
|
||||||
|
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
|
||||||
|
properties: node_properties::curves_properties,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
DocumentNodeType {
|
DocumentNodeType {
|
||||||
name: "Threshold",
|
name: "Threshold",
|
||||||
category: "Image Adjustments",
|
category: "Image Adjustments",
|
||||||
|
|
|
||||||
|
|
@ -589,6 +589,25 @@ fn color_widget(document_node: &DocumentNode, node_id: u64, index: usize, name:
|
||||||
}
|
}
|
||||||
LayoutGroup::Row { widgets }
|
LayoutGroup::Row { widgets }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn curves_widget(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
|
||||||
|
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
|
||||||
|
|
||||||
|
if let NodeInput::Value {
|
||||||
|
tagged_value: TaggedValue::Curve(curve),
|
||||||
|
exposed: false,
|
||||||
|
} = &document_node.inputs[index]
|
||||||
|
{
|
||||||
|
widgets.extend_from_slice(&[
|
||||||
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
|
CurveInput::new(curve.clone())
|
||||||
|
.on_update(update_value(|x: &CurveInput| TaggedValue::Curve(x.value.clone()), node_id, index))
|
||||||
|
.widget_holder(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
LayoutGroup::Row { widgets }
|
||||||
|
}
|
||||||
|
|
||||||
/// Properties for the input node, with information describing how frames work and a refresh button
|
/// Properties for the input node, with information describing how frames work and a refresh button
|
||||||
pub fn input_properties(_document_node: &DocumentNode, _node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
pub fn input_properties(_document_node: &DocumentNode, _node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
let information = TextLabel::new("The graph's input frame is the rasterized artwork under the layer").widget_holder();
|
let information = TextLabel::new("The graph's input frame is the rasterized artwork under the layer").widget_holder();
|
||||||
|
|
@ -777,6 +796,12 @@ pub fn brightness_contrast_properties(document_node: &DocumentNode, node_id: Nod
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn curves_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
|
let curves = curves_widget(document_node, node_id, 1, "Curve", true);
|
||||||
|
|
||||||
|
vec![curves]
|
||||||
|
}
|
||||||
|
|
||||||
pub fn _blur_image_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
pub fn _blur_image_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
let radius = number_widget(document_node, node_id, 1, "Radius", NumberInput::default().min(0.).max(20.).int(), true);
|
let radius = number_widget(document_node, node_id, 1, "Radius", NumberInput::default().min(0.).max(20.).int(), true);
|
||||||
let sigma = number_widget(document_node, node_id, 2, "Sigma", NumberInput::default().min(0.).max(10000.), true);
|
let sigma = number_widget(document_node, node_id, 2, "Sigma", NumberInput::default().min(0.).max(10000.), true);
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ use crate::messages::portfolio::document::node_graph::VectorDataModification;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
use bezier_rs::{Bezier, TValue};
|
use bezier_rs::{Bezier, TValue};
|
||||||
|
use document_legacy::document::Document;
|
||||||
use document_legacy::LayerId;
|
use document_legacy::LayerId;
|
||||||
use graphene_core::uuid::ManipulatorGroupId;
|
use graphene_core::uuid::ManipulatorGroupId;
|
||||||
use graphene_core::vector::{ManipulatorPointId, SelectedType, VectorData};
|
use graphene_core::vector::{ManipulatorPointId, SelectedType, VectorData};
|
||||||
|
|
||||||
use document_legacy::document::Document;
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||||
import CheckboxInput from "@graphite/components/widgets/inputs/CheckboxInput.svelte";
|
import CheckboxInput from "@graphite/components/widgets/inputs/CheckboxInput.svelte";
|
||||||
import ColorInput from "@graphite/components/widgets/inputs/ColorInput.svelte";
|
import ColorInput from "@graphite/components/widgets/inputs/ColorInput.svelte";
|
||||||
|
import CurveInput from "@graphite/components/widgets/inputs/CurveInput.svelte";
|
||||||
import DropdownInput from "@graphite/components/widgets/inputs/DropdownInput.svelte";
|
import DropdownInput from "@graphite/components/widgets/inputs/DropdownInput.svelte";
|
||||||
import FontInput from "@graphite/components/widgets/inputs/FontInput.svelte";
|
import FontInput from "@graphite/components/widgets/inputs/FontInput.svelte";
|
||||||
import LayerReferenceInput from "@graphite/components/widgets/inputs/LayerReferenceInput.svelte";
|
import LayerReferenceInput from "@graphite/components/widgets/inputs/LayerReferenceInput.svelte";
|
||||||
|
|
@ -95,6 +96,10 @@
|
||||||
{#if colorInput}
|
{#if colorInput}
|
||||||
<ColorInput {...exclude(colorInput)} on:value={({ detail }) => updateLayout(index, detail)} sharpRightCorners={nextIsSuffix} />
|
<ColorInput {...exclude(colorInput)} on:value={({ detail }) => updateLayout(index, detail)} sharpRightCorners={nextIsSuffix} />
|
||||||
{/if}
|
{/if}
|
||||||
|
{@const curvesInput = narrowWidgetProps(component.props, "CurveInput")}
|
||||||
|
{#if curvesInput}
|
||||||
|
<CurveInput {...exclude(curvesInput)} on:value={({ detail }) => debouncer((value) => updateLayout(index, value), { debounceTime: 120 }).updateValue(detail)} />
|
||||||
|
{/if}
|
||||||
{@const dropdownInput = narrowWidgetProps(component.props, "DropdownInput")}
|
{@const dropdownInput = narrowWidgetProps(component.props, "DropdownInput")}
|
||||||
{#if dropdownInput}
|
{#if dropdownInput}
|
||||||
<DropdownInput {...exclude(dropdownInput)} on:selectedIndex={({ detail }) => updateLayout(index, detail)} sharpRightCorners={nextIsSuffix} />
|
<DropdownInput {...exclude(dropdownInput)} on:selectedIndex={({ detail }) => updateLayout(index, detail)} sharpRightCorners={nextIsSuffix} />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
import type { Curve, CurveManipulatorGroup } from "@graphite/wasm-communication/messages";
|
||||||
|
|
||||||
|
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||||
|
import { clamp } from "@graphite/utility-functions/math";
|
||||||
|
|
||||||
|
// emits: ["update:value"],
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
value: Curve;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
export let classes: Record<string, boolean> = {};
|
||||||
|
let styleName = "";
|
||||||
|
export { styleName as style };
|
||||||
|
export let styles: Record<string, string | number | undefined> = {};
|
||||||
|
export let value: Curve;
|
||||||
|
export let disabled = false;
|
||||||
|
export let tooltip: string | undefined = undefined;
|
||||||
|
|
||||||
|
const GRID_SIZE = 4;
|
||||||
|
|
||||||
|
let groups: CurveManipulatorGroup[] = [
|
||||||
|
{
|
||||||
|
anchor: [0, 0],
|
||||||
|
handles: [
|
||||||
|
[-1, -1],
|
||||||
|
[0.25, 0.25],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: [0.5, 0.5],
|
||||||
|
handles: [
|
||||||
|
[0.25, 0.25],
|
||||||
|
[0.75, 0.75],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anchor: [1, 1],
|
||||||
|
handles: [
|
||||||
|
[0.75, 0.75],
|
||||||
|
[2, 2],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let selectedNodeIndex: number | undefined = undefined;
|
||||||
|
let draggedNodeIndex: number | undefined = undefined;
|
||||||
|
let dAttribute = recalculateSvgPath();
|
||||||
|
|
||||||
|
$: {
|
||||||
|
groups = [groups[0]].concat(value.manipulatorGroups).concat([groups[groups.length - 1]]);
|
||||||
|
groups[0].handles[1] = value.firstHandle;
|
||||||
|
groups[groups.length - 1].handles[0] = value.lastHandle;
|
||||||
|
dAttribute = recalculateSvgPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurve() {
|
||||||
|
dispatch("value", {
|
||||||
|
manipulatorGroups: groups.slice(1, groups.length - 1),
|
||||||
|
firstHandle: groups[0].handles[1],
|
||||||
|
lastHandle: groups[groups.length - 1].handles[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalculateSvgPath() {
|
||||||
|
let dAttribute = "";
|
||||||
|
let anchor = groups[0].anchor;
|
||||||
|
let handle = groups[0].handles[1];
|
||||||
|
|
||||||
|
groups.slice(1).forEach((group) => {
|
||||||
|
dAttribute += `M${anchor[0]} ${1 - anchor[1]} C${handle[0]} ${1 - handle[1]}, ${group.handles[0][0]} ${1 - group.handles[0][1]}, ${group.anchor[0]} ${1 - group.anchor[1]} `;
|
||||||
|
anchor = group.anchor;
|
||||||
|
handle = group.handles[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
return dAttribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleManipulatorPointerDown(e: PointerEvent, i: number) {
|
||||||
|
// Delete an anchor with RMB or MMB
|
||||||
|
if (e.button > 0 && i > 0 && i < groups.length - 1) {
|
||||||
|
draggedNodeIndex = undefined;
|
||||||
|
selectedNodeIndex = undefined;
|
||||||
|
|
||||||
|
groups.splice(i, 1);
|
||||||
|
groups = groups;
|
||||||
|
|
||||||
|
dAttribute = recalculateSvgPath();
|
||||||
|
|
||||||
|
updateCurve();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
draggedNodeIndex = i;
|
||||||
|
if (i >= 0) selectedNodeIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSvgPositionFromPointerEvent(e: PointerEvent): [number, number] | undefined {
|
||||||
|
if (!(e.target instanceof SVGElement)) return undefined;
|
||||||
|
|
||||||
|
const target = e.target?.closest("svg") || undefined;
|
||||||
|
if (!target) return undefined;
|
||||||
|
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
const x = (e.x - rect.x) / rect.width;
|
||||||
|
const y = 1 - (e.y - rect.y) / rect.height;
|
||||||
|
return [clamp(x), clamp(y)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampHandles() {
|
||||||
|
for (let i = 0; i < groups.length - 1; i++) {
|
||||||
|
const [min, max] = [groups[i].anchor[0], groups[i + 1].anchor[0]];
|
||||||
|
|
||||||
|
for (let j = 0; j < 2; j++) {
|
||||||
|
groups[i + j].handles[1 - j][0] = clamp(groups[i + j].handles[1 - j][0], min, max);
|
||||||
|
groups[i + j].handles[1 - j][1] = clamp(groups[i + j].handles[1 - j][1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerUp(e: PointerEvent) {
|
||||||
|
if (draggedNodeIndex !== undefined) {
|
||||||
|
draggedNodeIndex = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
const anchor = getSvgPositionFromPointerEvent(e);
|
||||||
|
if (!anchor) return;
|
||||||
|
|
||||||
|
let nodeIndex = groups.findIndex((group) => group.anchor[0] > anchor[0]);
|
||||||
|
if (nodeIndex === -1) nodeIndex = groups.length;
|
||||||
|
|
||||||
|
groups.splice(nodeIndex, 0, {
|
||||||
|
anchor: anchor,
|
||||||
|
handles: [
|
||||||
|
[anchor[0] - 0.05, anchor[1]],
|
||||||
|
[anchor[0] + 0.05, anchor[1]],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
selectedNodeIndex = nodeIndex;
|
||||||
|
clampHandles();
|
||||||
|
dAttribute = recalculateSvgPath();
|
||||||
|
updateCurve();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHandlePosition(anchorIndex: number, handleIndex: number, position: [number, number]): void {
|
||||||
|
const { anchor, handles } = groups[anchorIndex];
|
||||||
|
const otherHandle = handles[1 - handleIndex];
|
||||||
|
|
||||||
|
const handleVector = [anchor[0] - position[0], anchor[1] - position[1]];
|
||||||
|
const handleVectorLength = Math.hypot(...handleVector);
|
||||||
|
const handleVectorNormalized = [handleVector[0] / handleVectorLength, handleVector[1] / handleVectorLength];
|
||||||
|
const otherHandleVectorLength = Math.hypot(anchor[0] - otherHandle[0], anchor[1] - otherHandle[1]);
|
||||||
|
|
||||||
|
handles[handleIndex] = position;
|
||||||
|
handles[1 - handleIndex] = [anchor[0] + handleVectorNormalized[0] * otherHandleVectorLength, anchor[1] + handleVectorNormalized[1] * otherHandleVectorLength];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(e: PointerEvent) {
|
||||||
|
if (draggedNodeIndex === undefined || draggedNodeIndex === 0 || draggedNodeIndex === groups.length - 1) return;
|
||||||
|
const position = getSvgPositionFromPointerEvent(e);
|
||||||
|
if (!position) return;
|
||||||
|
|
||||||
|
if (draggedNodeIndex > 0) {
|
||||||
|
position[0] = clamp(position[0], groups[draggedNodeIndex - 1].anchor[0], groups[draggedNodeIndex + 1].anchor[0]);
|
||||||
|
|
||||||
|
const group = groups[draggedNodeIndex];
|
||||||
|
group.handles = [
|
||||||
|
[group.handles[0][0] + position[0] - group.anchor[0], group.handles[0][1] + position[1] - group.anchor[1]],
|
||||||
|
[group.handles[1][0] + position[0] - group.anchor[0], group.handles[1][1] + position[1] - group.anchor[1]],
|
||||||
|
];
|
||||||
|
group.anchor = position;
|
||||||
|
} else {
|
||||||
|
if (selectedNodeIndex === undefined) return;
|
||||||
|
setHandlePosition(selectedNodeIndex, -draggedNodeIndex - 1, position);
|
||||||
|
|
||||||
|
const group = groups[selectedNodeIndex];
|
||||||
|
if (group.handles[0][0] > group.anchor[0]) {
|
||||||
|
group.handles = [group.handles[1], group.handles[0]];
|
||||||
|
draggedNodeIndex = -3 - draggedNodeIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clampHandles();
|
||||||
|
dAttribute = recalculateSvgPath();
|
||||||
|
updateCurve();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LayoutRow class={"curve-input"} classes={{ disabled, ...classes }} style={styleName} {styles} {tooltip}>
|
||||||
|
<svg viewBox="0 0 1 1" on:pointermove={handlePointerMove} on:pointerup={handlePointerUp}>
|
||||||
|
{#each { length: GRID_SIZE - 1 } as _, i}
|
||||||
|
<path class="grid" d={`M 0 ${(i + 1) / GRID_SIZE} L 1 ${(i + 1) / GRID_SIZE}`} />
|
||||||
|
<path class="grid" d={`M ${(i + 1) / GRID_SIZE} 0 L ${(i + 1) / GRID_SIZE} 1`} />
|
||||||
|
{/each}
|
||||||
|
<path class="curve" d={dAttribute} />
|
||||||
|
{#if selectedNodeIndex !== undefined}
|
||||||
|
{@const group = groups[selectedNodeIndex]}
|
||||||
|
{#each [0, 1] as i}
|
||||||
|
<path d={`M ${group.anchor[0]} ${1 - group.anchor[1]} L ${group.handles[i][0]} ${1 - group.handles[i][1]}`} class="handle-line" />
|
||||||
|
<circle cx={group.handles[i][0]} cy={1 - group.handles[i][1]} class="manipulator handle" on:pointerdown={(e) => handleManipulatorPointerDown(e, -i - 1)} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#each groups as group, i}
|
||||||
|
<circle cx={group.anchor[0]} cy={1 - group.anchor[1]} class="manipulator" on:pointerdown={(e) => handleManipulatorPointerDown(e, i)} />
|
||||||
|
{/each}
|
||||||
|
</svg>
|
||||||
|
<slot />
|
||||||
|
</LayoutRow>
|
||||||
|
|
||||||
|
<style lang="scss" global>
|
||||||
|
.curve-input {
|
||||||
|
background: var(--color-1-nearblack);
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
min-width: calc(2 * var(--widget-height));
|
||||||
|
max-width: calc(8 * var(--widget-height));
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
stroke: var(--color-7-middlegray);
|
||||||
|
stroke-width: 0.005;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.curve {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--color-e-nearwhite);
|
||||||
|
stroke-width: 0.01;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manipulator {
|
||||||
|
fill: var(--color-1-nearblack);
|
||||||
|
stroke: var(--color-e-nearwhite);
|
||||||
|
stroke-width: 0.01;
|
||||||
|
r: 0.02;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: var(--color-f-white);
|
||||||
|
stroke: var(--color-f-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.handle {
|
||||||
|
fill: var(--color-1-nearblack);
|
||||||
|
stroke: var(--color-c-brightgray);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: var(--color-a-softgray);
|
||||||
|
stroke: var(--color-a-softgray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-line {
|
||||||
|
stroke: var(--color-7-middlegray);
|
||||||
|
stroke-width: 0.005;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -7,23 +7,23 @@ export type DebouncerOptions = {
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export function debouncer<T>(callFn: (value: T) => unknown, { debounceTime = 60 }: Partial<DebouncerOptions> = {}) {
|
export function debouncer<T>(callFn: (value: T) => unknown, { debounceTime = 60 }: Partial<DebouncerOptions> = {}) {
|
||||||
let currentValue: T | undefined;
|
let currentValue: T | undefined;
|
||||||
|
let recentlyUpdated: boolean = false;
|
||||||
|
|
||||||
const emitValue = (): void => {
|
const emitValue = (): void => {
|
||||||
if (currentValue === undefined) {
|
recentlyUpdated = false;
|
||||||
throw new Error("Tried to emit undefined value from debouncer. This should never be possible");
|
if (currentValue === undefined) return;
|
||||||
}
|
updateValue(currentValue);
|
||||||
const emittingValue = currentValue;
|
|
||||||
currentValue = undefined;
|
|
||||||
callFn(emittingValue);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateValue = (newValue: T): void => {
|
const updateValue = (newValue: T): void => {
|
||||||
if (currentValue !== undefined) {
|
if (recentlyUpdated) {
|
||||||
currentValue = newValue;
|
currentValue = newValue;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentValue = newValue;
|
callFn(newValue);
|
||||||
|
recentlyUpdated = true;
|
||||||
|
currentValue = undefined;
|
||||||
setTimeout(emitValue, debounceTime);
|
setTimeout(emitValue, debounceTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -800,6 +800,26 @@ export type MenuListEntry = MenuEntryCommon & {
|
||||||
ref?: any;
|
ref?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class CurveManipulatorGroup {
|
||||||
|
anchor!: [number, number];
|
||||||
|
handles!: [[number, number], [number, number]];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Curve {
|
||||||
|
manipulatorGroups!: CurveManipulatorGroup[];
|
||||||
|
firstHandle!: [number, number];
|
||||||
|
lastHandle!: [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CurveInput extends WidgetProps {
|
||||||
|
value!: Curve;
|
||||||
|
|
||||||
|
disabled!: boolean;
|
||||||
|
|
||||||
|
@Transform(({ value }: { value: string }) => value || undefined)
|
||||||
|
tooltip!: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export class DropdownInput extends WidgetProps {
|
export class DropdownInput extends WidgetProps {
|
||||||
entries!: MenuListEntry[][];
|
entries!: MenuListEntry[][];
|
||||||
|
|
||||||
|
|
@ -1091,6 +1111,7 @@ const widgetSubTypes = [
|
||||||
{ value: BreadcrumbTrailButtons, name: "BreadcrumbTrailButtons" },
|
{ value: BreadcrumbTrailButtons, name: "BreadcrumbTrailButtons" },
|
||||||
{ value: CheckboxInput, name: "CheckboxInput" },
|
{ value: CheckboxInput, name: "CheckboxInput" },
|
||||||
{ value: ColorInput, name: "ColorInput" },
|
{ value: ColorInput, name: "ColorInput" },
|
||||||
|
{ value: CurveInput, name: "CurveInput" },
|
||||||
{ value: DropdownInput, name: "DropdownInput" },
|
{ value: DropdownInput, name: "DropdownInput" },
|
||||||
{ value: FontInput, name: "FontInput" },
|
{ value: FontInput, name: "FontInput" },
|
||||||
{ value: IconButton, name: "IconButton" },
|
{ value: IconButton, name: "IconButton" },
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::fmt::Write;
|
||||||
/// Functionality relating to core `Bezier` operations, such as constructors and `abs_diff_eq`.
|
/// Functionality relating to core `Bezier` operations, such as constructors and `abs_diff_eq`.
|
||||||
impl Bezier {
|
impl Bezier {
|
||||||
// TODO: Consider removing this function
|
// TODO: Consider removing this function
|
||||||
/// Create a quadratic bezier using the provided coordinates as the start, handle, and end points.
|
/// Create a linear bezier using the provided coordinates as the start and end points.
|
||||||
pub fn from_linear_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
|
pub fn from_linear_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
|
||||||
Bezier {
|
Bezier {
|
||||||
start: DVec2::new(x1, y1),
|
start: DVec2::new(x1, y1),
|
||||||
|
|
@ -13,7 +13,7 @@ impl Bezier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a quadratic bezier using the provided DVec2s as the start, handle, and end points.
|
/// Create a linear bezier using the provided DVec2s as the start and end points.
|
||||||
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/constructor/solo" title="Constructor Demo"></iframe>
|
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/constructor/solo" title="Constructor Demo"></iframe>
|
||||||
pub fn from_linear_dvec2(p1: DVec2, p2: DVec2) -> Self {
|
pub fn from_linear_dvec2(p1: DVec2, p2: DVec2) -> Self {
|
||||||
Bezier {
|
Bezier {
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,40 @@ impl Bezier {
|
||||||
min_corner.x <= bounding_box_min.x && min_corner.y <= bounding_box_min.y && bounding_box_max.x <= max_corner.x && bounding_box_max.y <= max_corner.y
|
min_corner.x <= bounding_box_min.x && min_corner.y <= bounding_box_min.y && bounding_box_max.x <= max_corner.x && bounding_box_max.y <= max_corner.y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an `Iterator` containing all possible parametric `t`-values at the given `x`-coordinate.
|
||||||
|
pub fn find_tvalues_for_x(&self, x: f64) -> impl Iterator<Item = f64> {
|
||||||
|
// Compute the roots of the resulting bezier curve
|
||||||
|
match self.handles {
|
||||||
|
BezierHandles::Linear => {
|
||||||
|
// If the transformed linear bezier is on the x-axis, `a` and `b` will both be zero and `solve_linear` will return no roots
|
||||||
|
let a = self.end.x - self.start.x;
|
||||||
|
let b = self.start.x - x;
|
||||||
|
utils::solve_linear(a, b)
|
||||||
|
}
|
||||||
|
BezierHandles::Quadratic { handle } => {
|
||||||
|
let a = self.start.x - 2. * handle.x + self.end.x;
|
||||||
|
let b = 2. * (handle.x - self.start.x);
|
||||||
|
let c = self.start.x - x;
|
||||||
|
|
||||||
|
let discriminant = b * b - 4. * a * c;
|
||||||
|
let two_times_a = 2. * a;
|
||||||
|
|
||||||
|
utils::solve_quadratic(discriminant, two_times_a, b, c)
|
||||||
|
}
|
||||||
|
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||||
|
let start_x = self.start.x;
|
||||||
|
let a = -start_x + 3. * handle_start.x - 3. * handle_end.x + self.end.x;
|
||||||
|
let b = 3. * start_x - 6. * handle_start.x + 3. * handle_end.x;
|
||||||
|
let c = -3. * start_x + 3. * handle_start.x;
|
||||||
|
let d = start_x - x;
|
||||||
|
|
||||||
|
utils::solve_cubic(a, b, c, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into_iter()
|
||||||
|
.filter(|&t| utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||||
/// Returns list of `t`-values representing the inflection points of the curve.
|
/// Returns list of `t`-values representing the inflection points of the curve.
|
||||||
/// The inflection points are defined to be points at which the second derivative of the curve is equal to zero.
|
/// The inflection points are defined to be points at which the second derivative of the curve is equal to zero.
|
||||||
|
|
@ -281,54 +315,24 @@ impl Bezier {
|
||||||
if other.handles == BezierHandles::Linear {
|
if other.handles == BezierHandles::Linear {
|
||||||
// Rotate the bezier and the line by the angle that the line makes with the x axis
|
// Rotate the bezier and the line by the angle that the line makes with the x axis
|
||||||
let line_directional_vector = other.end - other.start;
|
let line_directional_vector = other.end - other.start;
|
||||||
let angle = line_directional_vector.angle_between(DVec2::new(1., 0.));
|
let angle = line_directional_vector.angle_between(DVec2::new(0., 1.));
|
||||||
let rotation_matrix = DMat2::from_angle(angle);
|
let rotation_matrix = DMat2::from_angle(angle);
|
||||||
let rotated_bezier = self.apply_transformation(|point| rotation_matrix.mul_vec2(point));
|
let rotated_bezier = self.apply_transformation(|point| rotation_matrix.mul_vec2(point));
|
||||||
let rotated_line = [rotation_matrix.mul_vec2(other.start), rotation_matrix.mul_vec2(other.end)];
|
let rotated_line = [rotation_matrix.mul_vec2(other.start), rotation_matrix.mul_vec2(other.end)];
|
||||||
|
|
||||||
// Translate the bezier such that the line becomes aligned on top of the x-axis
|
// Translate the bezier such that the line becomes aligned on top of the x-axis
|
||||||
let vertical_distance = rotated_line[0].y;
|
let vertical_distance = rotated_line[0].x;
|
||||||
let translated_bezier = rotated_bezier.translate(DVec2::new(0., -vertical_distance));
|
let translated_bezier = rotated_bezier.translate(DVec2::new(-vertical_distance, 0.));
|
||||||
|
|
||||||
// Compute the roots of the resulting bezier curve
|
// Compute the roots of the resulting bezier curve
|
||||||
let list_intersection_t = match translated_bezier.handles {
|
let list_intersection_t = translated_bezier.find_tvalues_for_x(0.);
|
||||||
BezierHandles::Linear => {
|
|
||||||
// If the transformed linear bezier is on the x-axis, `a` and `b` will both be zero and `solve_linear` will return no roots
|
|
||||||
let a = translated_bezier.end.y - translated_bezier.start.y;
|
|
||||||
let b = translated_bezier.start.y;
|
|
||||||
utils::solve_linear(a, b)
|
|
||||||
}
|
|
||||||
BezierHandles::Quadratic { handle } => {
|
|
||||||
let a = translated_bezier.start.y - 2. * handle.y + translated_bezier.end.y;
|
|
||||||
let b = 2. * (handle.y - translated_bezier.start.y);
|
|
||||||
let c = translated_bezier.start.y;
|
|
||||||
|
|
||||||
let discriminant = b * b - 4. * a * c;
|
|
||||||
let two_times_a = 2. * a;
|
|
||||||
|
|
||||||
utils::solve_quadratic(discriminant, two_times_a, b, c)
|
|
||||||
}
|
|
||||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
|
||||||
let start_y = translated_bezier.start.y;
|
|
||||||
let a = -start_y + 3. * handle_start.y - 3. * handle_end.y + translated_bezier.end.y;
|
|
||||||
let b = 3. * start_y - 6. * handle_start.y + 3. * handle_end.y;
|
|
||||||
let c = -3. * start_y + 3. * handle_start.y;
|
|
||||||
let d = start_y;
|
|
||||||
|
|
||||||
utils::solve_cubic(a, b, c, d)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let min = other.start.min(other.end);
|
let min = other.start.min(other.end);
|
||||||
let max = other.start.max(other.end);
|
let max = other.start.max(other.end);
|
||||||
|
|
||||||
return list_intersection_t
|
return list_intersection_t
|
||||||
.into_iter()
|
|
||||||
// Accept the t value if it is approximately in [0, 1] and if the corresponding coordinates are within the range of the linear line
|
// Accept the t value if it is approximately in [0, 1] and if the corresponding coordinates are within the range of the linear line
|
||||||
.filter(|&t| {
|
.filter(|&t| utils::dvec2_approximately_in_range(self.unrestricted_parametric_evaluate(t), min, max, MAX_ABSOLUTE_DIFFERENCE).all())
|
||||||
utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE)
|
|
||||||
&& utils::dvec2_approximately_in_range(self.unrestricted_parametric_evaluate(t), min, max, MAX_ABSOLUTE_DIFFERENCE).all()
|
|
||||||
})
|
|
||||||
// Ensure the returned value is within the correct range
|
// Ensure the returned value is within the correct range
|
||||||
.map(|t| t.clamp(0., 1.))
|
.map(|t| t.clamp(0., 1.))
|
||||||
.collect::<Vec<f64>>();
|
.collect::<Vec<f64>>();
|
||||||
|
|
@ -724,6 +728,62 @@ mod tests {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_tvalues_for_x() {
|
||||||
|
struct Assertion {
|
||||||
|
bezier: Bezier,
|
||||||
|
x: f64,
|
||||||
|
ys: &'static [f64],
|
||||||
|
}
|
||||||
|
|
||||||
|
let assertions = [
|
||||||
|
Assertion {
|
||||||
|
bezier: Bezier::from_linear_coordinates(0., 0., 20., 10.),
|
||||||
|
x: 5.,
|
||||||
|
ys: &[2.5],
|
||||||
|
},
|
||||||
|
Assertion {
|
||||||
|
bezier: Bezier::from_quadratic_coordinates(0., 0., 10., 5., 20., 10.),
|
||||||
|
x: 5.,
|
||||||
|
ys: &[2.5],
|
||||||
|
},
|
||||||
|
Assertion {
|
||||||
|
bezier: Bezier::from_cubic_coordinates(0., 0., 10., 5., 10., 5., 20., 10.),
|
||||||
|
x: 5.,
|
||||||
|
ys: &[2.5],
|
||||||
|
},
|
||||||
|
Assertion {
|
||||||
|
bezier: Bezier::from_cubic_coordinates(90., 70., 25., 25., 175., 175., 110., 130.),
|
||||||
|
x: 100.,
|
||||||
|
ys: &[100.],
|
||||||
|
},
|
||||||
|
Assertion {
|
||||||
|
bezier: Bezier::from_cubic_coordinates(90., 70., 25., 25., 175., 175., 110., 130.),
|
||||||
|
x: 80.,
|
||||||
|
ys: &[63.62683, 74.53867],
|
||||||
|
},
|
||||||
|
Assertion {
|
||||||
|
bezier: Bezier::from_cubic_coordinates(110., 70., 25., 25., 175., 175., 90., 130.),
|
||||||
|
x: 100.,
|
||||||
|
ys: &[65.11345, 100., 134.88655],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for Assertion { bezier, x, ys } in assertions {
|
||||||
|
let mut got: Vec<f64> = bezier
|
||||||
|
.find_tvalues_for_x(x)
|
||||||
|
.map(|t| bezier.evaluate(TValue::Parametric(t)))
|
||||||
|
.inspect(|p| assert!((p.x - x).abs() < 1e-4, "wrong x-coordinate, got {} expected {x}", p.x))
|
||||||
|
.map(|p| p.y)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(got.len(), ys.len());
|
||||||
|
got.sort_by(f64::total_cmp);
|
||||||
|
got.into_iter()
|
||||||
|
.zip(ys)
|
||||||
|
.for_each(|(got, &expected)| assert!((got - expected).abs() < 1e-4, "wrong y-coordinate, got {got} expected {expected}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_inflections() {
|
fn test_inflections() {
|
||||||
let bezier = Bezier::from_cubic_coordinates(30., 30., 30., 150., 150., 30., 150., 150.);
|
let bezier = Bezier::from_cubic_coordinates(30., 30., 30., 150., 150., 30., 150., 150.);
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
use crate::raster::ImageFrame;
|
use crate::raster::ImageFrame;
|
||||||
use crate::transform::Transform;
|
use crate::text::FontCache;
|
||||||
use crate::transform::TransformMut;
|
use crate::transform::{Transform, TransformMut};
|
||||||
use crate::Color;
|
use crate::{Color, Node};
|
||||||
use crate::Node;
|
|
||||||
use alloc::sync::Arc;
|
|
||||||
use dyn_any::StaticType;
|
|
||||||
use dyn_any::StaticTypeSized;
|
|
||||||
use glam::DAffine2;
|
|
||||||
|
|
||||||
|
use dyn_any::{StaticType, StaticTypeSized};
|
||||||
|
|
||||||
|
use alloc::sync::Arc;
|
||||||
|
use core::fmt::Debug;
|
||||||
use core::future::Future;
|
use core::future::Future;
|
||||||
use core::hash::{Hash, Hasher};
|
use core::hash::{Hash, Hasher};
|
||||||
use core::pin::Pin;
|
use core::pin::Pin;
|
||||||
|
use glam::DAffine2;
|
||||||
use crate::text::FontCache;
|
|
||||||
|
|
||||||
use core::fmt::Debug;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ use crate::vector::VectorData;
|
||||||
use crate::{Color, Node};
|
use crate::{Color, Node};
|
||||||
|
|
||||||
use dyn_any::{DynAny, StaticType};
|
use dyn_any::{DynAny, StaticType};
|
||||||
|
use node_macro::node_fn;
|
||||||
|
|
||||||
use core::ops::{Deref, DerefMut};
|
use core::ops::{Deref, DerefMut};
|
||||||
use glam::IVec2;
|
use glam::IVec2;
|
||||||
use node_macro::node_fn;
|
|
||||||
|
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -146,9 +146,10 @@ impl<'i, 's: 'i, I: 'i, O: 'i, N: Node<'i, I, Output = O> + ?Sized> Node<'i, I>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use dyn_any::StaticTypeSized;
|
||||||
|
|
||||||
use core::pin::Pin;
|
use core::pin::Pin;
|
||||||
|
|
||||||
use dyn_any::StaticTypeSized;
|
|
||||||
#[cfg(feature = "alloc")]
|
#[cfg(feature = "alloc")]
|
||||||
impl<'i, I: 'i, O: 'i> Node<'i, I> for Pin<Box<dyn Node<'i, I, Output = O> + 'i>> {
|
impl<'i, I: 'i, O: 'i> Node<'i, I> for Pin<Box<dyn Node<'i, I, Output = O> + 'i>> {
|
||||||
type Output = O;
|
type Output = O;
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,24 @@ pub mod brightness_contrast;
|
||||||
#[cfg(not(target_arch = "spirv"))]
|
#[cfg(not(target_arch = "spirv"))]
|
||||||
pub mod brush_cache;
|
pub mod brush_cache;
|
||||||
pub mod color;
|
pub mod color;
|
||||||
|
pub mod curve;
|
||||||
pub mod discrete_srgb;
|
pub mod discrete_srgb;
|
||||||
pub use adjustments::*;
|
pub use adjustments::*;
|
||||||
|
|
||||||
#[cfg(target_arch = "spirv")]
|
|
||||||
use num_traits::Float;
|
|
||||||
|
|
||||||
pub trait Linear {
|
pub trait Linear {
|
||||||
fn from_f32(x: f32) -> Self;
|
fn from_f32(x: f32) -> Self;
|
||||||
fn to_f32(self) -> f32;
|
fn to_f32(self) -> f32;
|
||||||
fn from_f64(x: f64) -> Self;
|
fn from_f64(x: f64) -> Self;
|
||||||
fn to_f64(self) -> f64;
|
fn to_f64(self) -> f64;
|
||||||
|
fn lerp(self, other: Self, value: Self) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized + Copy,
|
||||||
|
Self: core::ops::Sub<Self, Output = Self>,
|
||||||
|
Self: core::ops::Mul<Self, Output = Self>,
|
||||||
|
Self: core::ops::Add<Self, Output = Self>,
|
||||||
|
{
|
||||||
|
self + (other - self) * value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
|
@ -191,6 +198,10 @@ pub trait Luminance {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait LuminanceMut: Luminance {
|
||||||
|
fn set_luminance(&mut self, luminance: Self::LuminanceChannel);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: We might rename this to Raster at some point
|
// TODO: We might rename this to Raster at some point
|
||||||
pub trait Sample {
|
pub trait Sample {
|
||||||
type Pixel: Pixel;
|
type Pixel: Pixel;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
#![allow(clippy::too_many_arguments)]
|
use super::curve::{Curve, CurveManipulatorGroup, ValueMapperNode};
|
||||||
use super::Color;
|
use super::{Channel, Color, Node};
|
||||||
use crate::Node;
|
|
||||||
use core::fmt::Debug;
|
use bezier_rs::{Bezier, TValue};
|
||||||
use dyn_any::{DynAny, StaticType};
|
use dyn_any::{DynAny, StaticType};
|
||||||
|
|
||||||
|
use core::fmt::Debug;
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[cfg(target_arch = "spirv")]
|
#[cfg(target_arch = "spirv")]
|
||||||
use spirv_std::num_traits::float::Float;
|
use spirv_std::num_traits::float::Float;
|
||||||
|
|
||||||
|
|
@ -204,7 +205,7 @@ pub struct ExtractAlphaNode;
|
||||||
#[node_macro::node_fn(ExtractAlphaNode)]
|
#[node_macro::node_fn(ExtractAlphaNode)]
|
||||||
fn extract_alpha_node(color: Color) -> Color {
|
fn extract_alpha_node(color: Color) -> Color {
|
||||||
let alpha = color.a();
|
let alpha = color.a();
|
||||||
Color::from_rgbaf32(alpha, alpha, alpha, 1.0).unwrap()
|
Color::from_rgbaf32(alpha, alpha, alpha, 1.).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
|
@ -215,7 +216,7 @@ fn extract_opaque_node(color: Color) -> Color {
|
||||||
if color.a() == 0. {
|
if color.a() == 0. {
|
||||||
return color.with_alpha(1.);
|
return color.with_alpha(1.);
|
||||||
}
|
}
|
||||||
Color::from_rgbaf32(color.r() / color.a(), color.g() / color.a(), color.b() / color.a(), 1.0).unwrap()
|
Color::from_rgbaf32(color.r() / color.a(), color.g() / color.a(), color.b() / color.a(), 1.).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
|
@ -856,6 +857,52 @@ fn exposure(color: Color, exposure: f32, offset: f32, gamma_correction: f32) ->
|
||||||
adjusted.map_rgb(|c: f32| c.clamp(0., 1.))
|
adjusted.map_rgb(|c: f32| c.clamp(0., 1.))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WINDOW_SIZE: usize = 1024;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct GenerateCurvesNode<OutputChannel, Curve> {
|
||||||
|
curve: Curve,
|
||||||
|
_channel: core::marker::PhantomData<OutputChannel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[node_macro::node_fn(GenerateCurvesNode<_Channel>)]
|
||||||
|
fn generate_curves<_Channel: Channel + super::Linear>(_primary: (), curve: Curve) -> ValueMapperNode<_Channel> {
|
||||||
|
let [mut pos, mut param]: [[f32; 2]; 2] = [[0.; 2], curve.first_handle];
|
||||||
|
let mut lut = vec![_Channel::from_f64(0.); WINDOW_SIZE];
|
||||||
|
let end = CurveManipulatorGroup {
|
||||||
|
anchor: [1.; 2],
|
||||||
|
handles: [curve.last_handle, [0.; 2]],
|
||||||
|
};
|
||||||
|
for sample in curve.manipulator_groups.iter().chain(core::iter::once(&end)) {
|
||||||
|
let [x0, y0, x1, y1, x2, y2, x3, y3] = [pos[0], pos[1], param[0], param[1], sample.handles[0][0], sample.handles[0][1], sample.anchor[0], sample.anchor[1]].map(f64::from);
|
||||||
|
|
||||||
|
let bezier = Bezier::from_cubic_coordinates(x0, y0, x1, y1, x2, y2, x3, y3);
|
||||||
|
|
||||||
|
let [left, right] = [pos[0], sample.anchor[0]].map(|c| c.clamp(0., 1.));
|
||||||
|
let lut_index_left: usize = (left * (lut.len() - 1) as f32).floor() as _;
|
||||||
|
let lut_index_right: usize = (right * (lut.len() - 1) as f32).ceil() as _;
|
||||||
|
for index in lut_index_left..=lut_index_right {
|
||||||
|
let x = index as f64 / (lut.len() - 1) as f64;
|
||||||
|
let y = if x <= x0 {
|
||||||
|
y0
|
||||||
|
} else if x >= x3 {
|
||||||
|
y3
|
||||||
|
} else {
|
||||||
|
bezier.find_tvalues_for_x(x)
|
||||||
|
.next()
|
||||||
|
.map(|t| bezier.evaluate(TValue::Parametric(t.clamp(0., 1.))).y)
|
||||||
|
// a very bad approximation if bezier_rs failes
|
||||||
|
.unwrap_or_else(|| (x - x0) / (x3 - x0) * (y3 - y0) + y0)
|
||||||
|
};
|
||||||
|
lut[index] = _Channel::from_f64(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = sample.anchor;
|
||||||
|
param = sample.handles[1];
|
||||||
|
}
|
||||||
|
ValueMapperNode::new(lut)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "alloc")]
|
#[cfg(feature = "alloc")]
|
||||||
pub use index_node::IndexNode;
|
pub use index_node::IndexNode;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ pub struct GenerateBrightnessContrastMapperNode<Brightness, Contrast> {
|
||||||
contrast: Contrast,
|
contrast: Contrast,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Replace this node implementation with one that uses the more generalized Curves adjustment node
|
||||||
#[node_macro::node_fn(GenerateBrightnessContrastMapperNode)]
|
#[node_macro::node_fn(GenerateBrightnessContrastMapperNode)]
|
||||||
fn brightness_contrast_node(_primary: (), brightness: f32, contrast: f32) -> BrightnessContrastMapperNode {
|
fn brightness_contrast_node(_primary: (), brightness: f32, contrast: f32) -> BrightnessContrastMapperNode {
|
||||||
// Brightness LUT
|
// Brightness LUT
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use bytemuck::{Pod, Zeroable};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float},
|
discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float},
|
||||||
Alpha, AssociatedAlpha, Luminance, Pixel, RGBMut, Rec709Primaries, RGB, SRGB,
|
Alpha, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGBMut, Rec709Primaries, RGB, SRGB,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
|
|
@ -113,6 +113,12 @@ impl Luminance for Luma {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LuminanceMut for Luma {
|
||||||
|
fn set_luminance(&mut self, luminance: Self::LuminanceChannel) {
|
||||||
|
self.0 = luminance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RGB for Luma {
|
impl RGB for Luma {
|
||||||
type ColorChannel = f32;
|
type ColorChannel = f32;
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
|
|
@ -230,6 +236,28 @@ impl Luminance for Color {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LuminanceMut for Color {
|
||||||
|
fn set_luminance(&mut self, luminance: f32) {
|
||||||
|
let current = self.luminance();
|
||||||
|
// When we have a black-ish color, we just set the color to a grey-scale value. This prohibits a divide-by-0.
|
||||||
|
if current < f32::EPSILON {
|
||||||
|
self.red = 0.2126 * luminance;
|
||||||
|
self.green = 0.7152 * luminance;
|
||||||
|
self.blue = 0.0722 * luminance;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let fac = luminance / current;
|
||||||
|
// TODO: when we have for example the rgb color (0, 0, 1) and want to
|
||||||
|
// TODO: do `.set_luminance(1)`, then the actual luminance is not 1 at
|
||||||
|
// TODO: the end. With no clamp, the resulting color would be
|
||||||
|
// TODO: (0, 0, 12.8504). The excess should be spread to the other
|
||||||
|
// TODO: channels, but is currently just clamped away.
|
||||||
|
self.red = (self.red * fac).clamp(0., 1.);
|
||||||
|
self.green = (self.green * fac).clamp(0., 1.);
|
||||||
|
self.blue = (self.blue * fac).clamp(0., 1.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Rec709Primaries for Color {}
|
impl Rec709Primaries for Color {}
|
||||||
impl SRGB for Color {}
|
impl SRGB for Color {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
use super::{Channel, Linear, LuminanceMut};
|
||||||
|
use crate::Node;
|
||||||
|
|
||||||
|
use dyn_any::{DynAny, StaticType};
|
||||||
|
|
||||||
|
use core::ops::{Add, Mul, Sub};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, DynAny, specta::Type)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct Curve {
|
||||||
|
#[serde(rename = "manipulatorGroups")]
|
||||||
|
pub manipulator_groups: Vec<CurveManipulatorGroup>,
|
||||||
|
#[serde(rename = "firstHandle")]
|
||||||
|
pub first_handle: [f32; 2],
|
||||||
|
#[serde(rename = "lastHandle")]
|
||||||
|
pub last_handle: [f32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Curve {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
manipulator_groups: vec![],
|
||||||
|
first_handle: [0.2; 2],
|
||||||
|
last_handle: [0.8; 2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::hash::Hash for Curve {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.manipulator_groups.hash(state);
|
||||||
|
[self.first_handle, self.last_handle].iter().flatten().for_each(|f| f.to_bits().hash(state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, DynAny, specta::Type)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct CurveManipulatorGroup {
|
||||||
|
pub anchor: [f32; 2],
|
||||||
|
pub handles: [[f32; 2]; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::hash::Hash for CurveManipulatorGroup {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
for c in self.handles.iter().chain([&self.anchor]).flatten() {
|
||||||
|
c.to_bits().hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CubicSplines {
|
||||||
|
pub x: [f32; 4],
|
||||||
|
pub y: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CubicSplines {
|
||||||
|
pub fn solve(&self) -> [f32; 4] {
|
||||||
|
let (x, y) = (&self.x, &self.y);
|
||||||
|
|
||||||
|
// Build an augmented matrix to solve the system of equations using Gaussian elimination
|
||||||
|
let mut augmented_matrix = [
|
||||||
|
[
|
||||||
|
2. / (x[1] - x[0]),
|
||||||
|
1. / (x[1] - x[0]),
|
||||||
|
0.,
|
||||||
|
0.,
|
||||||
|
// |
|
||||||
|
3. * (y[1] - y[0]) / ((x[1] - x[0]) * (x[1] - x[0])),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1. / (x[1] - x[0]),
|
||||||
|
2. * (1. / (x[1] - x[0]) + 1. / (x[2] - x[1])),
|
||||||
|
1. / (x[2] - x[1]),
|
||||||
|
0.,
|
||||||
|
// |
|
||||||
|
3. * ((y[1] - y[0]) / ((x[1] - x[0]) * (x[1] - x[0])) + (y[2] - y[1]) / ((x[2] - x[1]) * (x[2] - x[1]))),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.,
|
||||||
|
1. / (x[2] - x[1]),
|
||||||
|
2. * (1. / (x[2] - x[1]) + 1. / (x[3] - x[2])),
|
||||||
|
1. / (x[3] - x[2]),
|
||||||
|
// |
|
||||||
|
3. * ((y[2] - y[1]) / ((x[2] - x[1]) * (x[2] - x[1])) + (y[3] - y[2]) / ((x[3] - x[2]) * (x[3] - x[2]))),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.,
|
||||||
|
0.,
|
||||||
|
1. / (x[3] - x[2]),
|
||||||
|
2. / (x[3] - x[2]),
|
||||||
|
// |
|
||||||
|
3. * (y[3] - y[2]) / ((x[3] - x[2]) * (x[3] - x[2])),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Gaussian elimination: forward elimination
|
||||||
|
for row in 0..4 {
|
||||||
|
let pivot_row_index = (row..4)
|
||||||
|
.max_by(|&a_row, &b_row| {
|
||||||
|
augmented_matrix[a_row][row]
|
||||||
|
.abs()
|
||||||
|
.partial_cmp(&augmented_matrix[b_row][row].abs())
|
||||||
|
.unwrap_or(core::cmp::Ordering::Equal)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Swap the current row with the row that has the largest pivot element
|
||||||
|
augmented_matrix.swap(row, pivot_row_index);
|
||||||
|
|
||||||
|
// Eliminate the current column in all rows below the current one
|
||||||
|
for row_below_current in row + 1..4 {
|
||||||
|
assert!(augmented_matrix[row][row].abs() > core::f32::EPSILON);
|
||||||
|
|
||||||
|
let scale_factor = augmented_matrix[row_below_current][row] / augmented_matrix[row][row];
|
||||||
|
for col in row..5 {
|
||||||
|
augmented_matrix[row_below_current][col] -= augmented_matrix[row][col] * scale_factor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gaussian elimination: back substitution
|
||||||
|
let mut solutions = [0.; 4];
|
||||||
|
for col in (0..4).rev() {
|
||||||
|
assert!(augmented_matrix[col][col].abs() > core::f32::EPSILON);
|
||||||
|
|
||||||
|
solutions[col] = augmented_matrix[col][4] / augmented_matrix[col][col];
|
||||||
|
|
||||||
|
for row in (0..col).rev() {
|
||||||
|
augmented_matrix[row][4] -= augmented_matrix[row][col] * solutions[col];
|
||||||
|
augmented_matrix[row][col] = 0.;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
solutions
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interpolate(&self, input: f32, solutions: &[f32]) -> f32 {
|
||||||
|
if input <= self.x[0] {
|
||||||
|
return self.y[0];
|
||||||
|
}
|
||||||
|
if input >= self.x[self.x.len() - 1] {
|
||||||
|
return self.y[self.x.len() - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the segment that the input falls between
|
||||||
|
let mut segment = 1;
|
||||||
|
while self.x[segment] < input {
|
||||||
|
segment += 1;
|
||||||
|
}
|
||||||
|
let segment_start = segment - 1;
|
||||||
|
let segment_end = segment;
|
||||||
|
|
||||||
|
// Calculate the output value using quadratic interpolation
|
||||||
|
let input_value = self.x[segment_start];
|
||||||
|
let input_value_prev = self.x[segment_end];
|
||||||
|
let output_value = self.y[segment_start];
|
||||||
|
let output_value_prev = self.y[segment_end];
|
||||||
|
let solutions_value = solutions[segment_start];
|
||||||
|
let solutions_value_prev = solutions[segment_end];
|
||||||
|
|
||||||
|
let output_delta = solutions_value_prev * (input_value - input_value_prev) - (output_value - output_value_prev);
|
||||||
|
let solution_delta = (output_value - output_value_prev) - solutions_value * (input_value - input_value_prev);
|
||||||
|
|
||||||
|
let input_ratio = (input - input_value_prev) / (input_value - input_value_prev);
|
||||||
|
let prev_output_ratio = (1. - input_ratio) * output_value_prev;
|
||||||
|
let output_ratio = input_ratio * output_value;
|
||||||
|
let quadratic_ratio = input_ratio * (1. - input_ratio) * (output_delta * (1. - input_ratio) + solution_delta * input_ratio);
|
||||||
|
|
||||||
|
let result = prev_output_ratio + output_ratio + quadratic_ratio;
|
||||||
|
result.clamp(0., 1.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ValueMapperNode<C> {
|
||||||
|
lut: Vec<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> ValueMapperNode<C> {
|
||||||
|
pub const fn new(lut: Vec<C>) -> Self {
|
||||||
|
Self { lut }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'i, L: LuminanceMut + 'i> Node<'i, L> for ValueMapperNode<L::LuminanceChannel>
|
||||||
|
where
|
||||||
|
L::LuminanceChannel: Linear + Copy,
|
||||||
|
L::LuminanceChannel: Add<Output = L::LuminanceChannel>,
|
||||||
|
L::LuminanceChannel: Sub<Output = L::LuminanceChannel>,
|
||||||
|
L::LuminanceChannel: Mul<Output = L::LuminanceChannel>,
|
||||||
|
{
|
||||||
|
type Output = L;
|
||||||
|
|
||||||
|
fn eval(&'i self, mut val: L) -> L {
|
||||||
|
let luminance: f32 = val.luminance().to_linear();
|
||||||
|
let floating_sample_index = luminance * (self.lut.len() - 1) as f32;
|
||||||
|
let index_in_lut = floating_sample_index.floor() as usize;
|
||||||
|
let a = self.lut[index_in_lut];
|
||||||
|
let b = self.lut[(index_in_lut + 1).clamp(0, self.lut.len() - 1)];
|
||||||
|
let result = a.lerp(b, L::LuminanceChannel::from_linear(floating_sample_index.fract()));
|
||||||
|
val.set_luminance(result);
|
||||||
|
val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::uuid::ManipulatorGroupId;
|
use crate::uuid::ManipulatorGroupId;
|
||||||
|
|
||||||
use bezier_rs::{ManipulatorGroup, Subpath};
|
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder};
|
use rustybuzz::ttf_parser::{GlyphId, OutlineBuilder};
|
||||||
use rustybuzz::{GlyphBuffer, UnicodeBuffer};
|
use rustybuzz::{GlyphBuffer, UnicodeBuffer};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use crate::vector::VectorData;
|
||||||
use crate::Node;
|
use crate::Node;
|
||||||
|
|
||||||
use bezier_rs::Subpath;
|
use bezier_rs::Subpath;
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
|
||||||
pub struct UnitCircleGenerator;
|
pub struct UnitCircleGenerator;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use super::style::{PathStyle, Stroke};
|
use super::style::{PathStyle, Stroke};
|
||||||
use crate::{uuid::ManipulatorGroupId, Color};
|
use crate::uuid::ManipulatorGroupId;
|
||||||
|
use crate::Color;
|
||||||
|
|
||||||
use bezier_rs::ManipulatorGroup;
|
use bezier_rs::ManipulatorGroup;
|
||||||
use dyn_any::{DynAny, StaticType};
|
use dyn_any::{DynAny, StaticType};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use super::style::{Fill, FillType, Gradient, GradientType, Stroke};
|
use super::style::{Fill, FillType, Gradient, GradientType, Stroke};
|
||||||
use super::VectorData;
|
use super::VectorData;
|
||||||
use crate::{Color, Node};
|
use crate::{Color, Node};
|
||||||
|
|
||||||
use bezier_rs::Subpath;
|
use bezier_rs::Subpath;
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
|
@ -138,7 +140,7 @@ fn circular_repeat_vector_data(mut vector_data: VectorData, rotation_offset: f32
|
||||||
pub struct BoundingBoxNode;
|
pub struct BoundingBoxNode;
|
||||||
|
|
||||||
#[node_macro::node_fn(BoundingBoxNode)]
|
#[node_macro::node_fn(BoundingBoxNode)]
|
||||||
fn generate_bounding_box(mut vector_data: VectorData) -> VectorData {
|
fn generate_bounding_box(vector_data: VectorData) -> VectorData {
|
||||||
let bounding_box = vector_data.bounding_box().unwrap();
|
let bounding_box = vector_data.bounding_box().unwrap();
|
||||||
VectorData::from_subpaths(vec![Subpath::new_rect(
|
VectorData::from_subpaths(vec![Subpath::new_rect(
|
||||||
vector_data.transform.transform_point2(bounding_box[0]),
|
vector_data.transform.transform_point2(bounding_box[0]),
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ pub enum TaggedValue {
|
||||||
DocumentNode(DocumentNode),
|
DocumentNode(DocumentNode),
|
||||||
GraphicGroup(graphene_core::GraphicGroup),
|
GraphicGroup(graphene_core::GraphicGroup),
|
||||||
Artboard(graphene_core::Artboard),
|
Artboard(graphene_core::Artboard),
|
||||||
|
Curve(graphene_core::raster::curve::Curve),
|
||||||
IVec2(glam::IVec2),
|
IVec2(glam::IVec2),
|
||||||
SurfaceFrame(graphene_core::SurfaceFrame),
|
SurfaceFrame(graphene_core::SurfaceFrame),
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +127,7 @@ impl Hash for TaggedValue {
|
||||||
Self::DocumentNode(document_node) => document_node.hash(state),
|
Self::DocumentNode(document_node) => document_node.hash(state),
|
||||||
Self::GraphicGroup(graphic_group) => graphic_group.hash(state),
|
Self::GraphicGroup(graphic_group) => graphic_group.hash(state),
|
||||||
Self::Artboard(artboard) => artboard.hash(state),
|
Self::Artboard(artboard) => artboard.hash(state),
|
||||||
|
Self::Curve(curve) => curve.hash(state),
|
||||||
Self::IVec2(v) => v.hash(state),
|
Self::IVec2(v) => v.hash(state),
|
||||||
Self::SurfaceFrame(surface_id) => surface_id.hash(state),
|
Self::SurfaceFrame(surface_id) => surface_id.hash(state),
|
||||||
}
|
}
|
||||||
|
|
@ -179,6 +181,7 @@ impl<'a> TaggedValue {
|
||||||
TaggedValue::DocumentNode(x) => Box::new(x),
|
TaggedValue::DocumentNode(x) => Box::new(x),
|
||||||
TaggedValue::GraphicGroup(x) => Box::new(x),
|
TaggedValue::GraphicGroup(x) => Box::new(x),
|
||||||
TaggedValue::Artboard(x) => Box::new(x),
|
TaggedValue::Artboard(x) => Box::new(x),
|
||||||
|
TaggedValue::Curve(x) => Box::new(x),
|
||||||
TaggedValue::IVec2(x) => Box::new(x),
|
TaggedValue::IVec2(x) => Box::new(x),
|
||||||
TaggedValue::SurfaceFrame(x) => Box::new(x),
|
TaggedValue::SurfaceFrame(x) => Box::new(x),
|
||||||
}
|
}
|
||||||
|
|
@ -245,6 +248,7 @@ impl<'a> TaggedValue {
|
||||||
TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode),
|
TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode),
|
||||||
TaggedValue::GraphicGroup(_) => concrete!(graphene_core::GraphicGroup),
|
TaggedValue::GraphicGroup(_) => concrete!(graphene_core::GraphicGroup),
|
||||||
TaggedValue::Artboard(_) => concrete!(graphene_core::Artboard),
|
TaggedValue::Artboard(_) => concrete!(graphene_core::Artboard),
|
||||||
|
TaggedValue::Curve(_) => concrete!(graphene_core::raster::curve::Curve),
|
||||||
TaggedValue::IVec2(_) => concrete!(glam::IVec2),
|
TaggedValue::IVec2(_) => concrete!(glam::IVec2),
|
||||||
TaggedValue::SurfaceFrame(_) => concrete!(graphene_core::SurfaceFrame),
|
TaggedValue::SurfaceFrame(_) => concrete!(graphene_core::SurfaceFrame),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -420,7 +420,7 @@ impl ProtoNetwork {
|
||||||
let mut visited = HashSet::new();
|
let mut visited = HashSet::new();
|
||||||
|
|
||||||
let inwards_edges = self.collect_inwards_edges();
|
let inwards_edges = self.collect_inwards_edges();
|
||||||
for (id, node) in &self.nodes {
|
for (id, _node) in &self.nodes {
|
||||||
for &dependency in inwards_edges.get(id).unwrap_or(&Vec::new()) {
|
for &dependency in inwards_edges.get(id).unwrap_or(&Vec::new()) {
|
||||||
if !visited.contains(&dependency) {
|
if !visited.contains(&dependency) {
|
||||||
dbg!(id, dependency);
|
dbg!(id, dependency);
|
||||||
|
|
|
||||||
|
|
@ -476,6 +476,43 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|
||||||
},
|
},
|
||||||
NodeIOTypes::new(concrete!(ImageFrame<Color>), concrete!(ImageFrame<Color>), vec![fn_type!(f32), fn_type!(f32), fn_type!(bool)]),
|
NodeIOTypes::new(concrete!(ImageFrame<Color>), concrete!(ImageFrame<Color>), vec![fn_type!(f32), fn_type!(f32), fn_type!(bool)]),
|
||||||
)],
|
)],
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
NodeIdentifier::new("graphene_core::raster::CurvesNode<_>"),
|
||||||
|
|args| {
|
||||||
|
use graphene_core::raster::{curve::Curve, GenerateCurvesNode};
|
||||||
|
let curve: DowncastBothNode<(), Curve> = DowncastBothNode::new(args[0].clone());
|
||||||
|
Box::pin(async move {
|
||||||
|
let curve = ClonedNode::new(curve.eval(()).await);
|
||||||
|
|
||||||
|
let generate_curves_node = GenerateCurvesNode::<f32, _>::new(curve);
|
||||||
|
let map_image_frame_node = graphene_std::raster::MapImageNode::new(ValueNode::new(generate_curves_node.eval(())));
|
||||||
|
let map_image_frame_node = FutureWrapperNode::new(map_image_frame_node);
|
||||||
|
let any: DynAnyNode<ImageFrame<Luma>, _, _> = graphene_std::any::DynAnyNode::new(map_image_frame_node);
|
||||||
|
any.into_type_erased()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
NodeIOTypes::new(concrete!(ImageFrame<Luma>), concrete!(ImageFrame<Luma>), vec![fn_type!(graphene_core::raster::curve::Curve)]),
|
||||||
|
),
|
||||||
|
// TODO: Use channel split and merge for this instead of using LuminanceMut for the whole color.
|
||||||
|
(
|
||||||
|
NodeIdentifier::new("graphene_core::raster::CurvesNode<_>"),
|
||||||
|
|args| {
|
||||||
|
use graphene_core::raster::{curve::Curve, GenerateCurvesNode};
|
||||||
|
let curve: DowncastBothNode<(), Curve> = DowncastBothNode::new(args[0].clone());
|
||||||
|
Box::pin(async move {
|
||||||
|
let curve = ClonedNode::new(curve.eval(()).await);
|
||||||
|
|
||||||
|
let generate_curves_node = GenerateCurvesNode::<f32, _>::new(curve);
|
||||||
|
let map_image_frame_node = graphene_std::raster::MapImageNode::new(ValueNode::new(generate_curves_node.eval(())));
|
||||||
|
let map_image_frame_node = FutureWrapperNode::new(map_image_frame_node);
|
||||||
|
let any: DynAnyNode<ImageFrame<Color>, _, _> = graphene_std::any::DynAnyNode::new(map_image_frame_node);
|
||||||
|
any.into_type_erased()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
NodeIOTypes::new(concrete!(ImageFrame<Color>), concrete!(ImageFrame<Color>), vec![fn_type!(graphene_core::raster::curve::Curve)]),
|
||||||
|
),
|
||||||
|
],
|
||||||
raster_node!(graphene_core::raster::OpacityNode<_>, params: [f32]),
|
raster_node!(graphene_core::raster::OpacityNode<_>, params: [f32]),
|
||||||
raster_node!(graphene_core::raster::PosterizeNode<_>, params: [f32]),
|
raster_node!(graphene_core::raster::PosterizeNode<_>, params: [f32]),
|
||||||
raster_node!(graphene_core::raster::ExposureNode<_, _, _>, params: [f32, f32, f32]),
|
raster_node!(graphene_core::raster::ExposureNode<_, _, _>, params: [f32, f32, f32]),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use crate::svg_drawing::*;
|
||||||
use crate::utils::parse_cap;
|
use crate::utils::parse_cap;
|
||||||
|
|
||||||
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, TValue, TValueType};
|
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, TValue, TValueType};
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue