react-bezier-curve-editor
Version:
Simple bezier curve editor built with React
627 lines (616 loc) • 22.9 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __objRest = (source, exclude) => {
var target = {};
for (var prop in source)
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
target[prop] = source[prop];
if (source != null && __getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(source)) {
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
target[prop] = source[prop];
}
return target;
};
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// node_modules/classnames/index.js
var require_classnames = __commonJS({
"node_modules/classnames/index.js"(exports, module) {
(function() {
"use strict";
var hasOwn = {}.hasOwnProperty;
function classNames() {
var classes = [];
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (!arg)
continue;
var argType = typeof arg;
if (argType === "string" || argType === "number") {
classes.push(arg);
} else if (Array.isArray(arg)) {
if (arg.length) {
var inner = classNames.apply(null, arg);
if (inner) {
classes.push(inner);
}
}
} else if (argType === "object") {
if (arg.toString === Object.prototype.toString) {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
} else {
classes.push(arg.toString());
}
}
}
return classes.join(" ");
}
if (typeof module !== "undefined" && module.exports) {
classNames.default = classNames;
module.exports = classNames;
} else if (typeof define === "function" && typeof define.amd === "object" && define.amd) {
define("classnames", [], function() {
return classNames;
});
} else {
window.classNames = classNames;
}
})();
}
});
// src/utils/bezierCurveParamsFromSizeAndValue.ts
function bezierCurveParamsFromSizeAndValue(size, value) {
return {
startCoordinate: getStartPointCoordinate(size, value),
endCoordinate: getEndPointCoordinate(size, value),
startBezierHandle: getStartHandleCoordinate(size, value),
endBezierHandle: getEndHandleCoordinate(size, value)
};
}
function getStartPointCoordinate(size, value) {
return [size * value[0], size * (1 - value[1])];
}
function getStartHandleCoordinate(size, value) {
return [size * value[2], size * (1 - value[3])];
}
function getEndPointCoordinate(size, value) {
return [size * value[6], size * (1 - value[7])];
}
function getEndHandleCoordinate(size, value) {
return [size * value[4], size * (1 - value[5])];
}
// src/components/BezierCurveEditor/BezierCurveEditor.tsx
var import_classnames2 = __toESM(require_classnames(), 1);
import React5 from "react";
// src/components/BezierCurveEditor/BezierCurveEditor.module.css
var BezierCurveEditor_default = {
root: "BezierCurveEditor_root",
wrap: "BezierCurveEditor_wrap",
bg: "BezierCurveEditor_bg",
plane: "BezierCurveEditor_plane",
innerArea: "BezierCurveEditor_innerArea",
row: "BezierCurveEditor_row",
curve: "BezierCurveEditor_curve",
handleLine: "BezierCurveEditor_handleLine",
curveLine: "BezierCurveEditor_curveLine",
preview: "BezierCurveEditor_preview",
"preview-loop": "BezierCurveEditor_preview-loop",
handle: "BezierCurveEditor_handle",
active: "BezierCurveEditor_active",
fixed: "BezierCurveEditor_fixed",
start: "BezierCurveEditor_start",
end: "BezierCurveEditor_end"
};
// src/components/BezierCurveEditor/BezierCurveEditorPlane.tsx
import React from "react";
function BezierCurveEditorPlane({ size, outerAreaSize, strokeWidth, innerAreaColor, rowColor }) {
return /* @__PURE__ */ React.createElement(
"div",
{
className: BezierCurveEditor_default.plane,
style: {
top: `${outerAreaSize + strokeWidth}px`,
left: `${strokeWidth}px`,
width: `${size - strokeWidth}px`,
height: `${size}px`
}
},
/* @__PURE__ */ React.createElement("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", xmlns: "http://www.w3.org/2000/svg" }, /* @__PURE__ */ React.createElement("rect", { width: "100", height: "101", className: BezierCurveEditor_default.innerArea, fill: innerAreaColor }), /* @__PURE__ */ React.createElement("g", { className: BezierCurveEditor_default.row, fill: rowColor }, /* @__PURE__ */ React.createElement("rect", { width: "100", height: "5" }), /* @__PURE__ */ React.createElement("rect", { y: "10", width: "100", height: "5" }), /* @__PURE__ */ React.createElement("rect", { y: "20", width: "100", height: "5" }), /* @__PURE__ */ React.createElement("rect", { y: "30", width: "100", height: "5" }), /* @__PURE__ */ React.createElement("rect", { y: "40", width: "100", height: "5" }), /* @__PURE__ */ React.createElement("rect", { y: "50", width: "100", height: "5" }), /* @__PURE__ */ React.createElement("rect", { y: "60", width: "100", height: "5" }), /* @__PURE__ */ React.createElement("rect", { y: "70", width: "100", height: "5" }), /* @__PURE__ */ React.createElement("rect", { y: "80", width: "100", height: "5" }), /* @__PURE__ */ React.createElement("rect", { y: "90", width: "100", height: "5" })))
);
}
// src/components/BezierCurveEditor/BezierCurveEditorCurve.tsx
import React2 from "react";
function BezierCurveEditorCurve({
value,
size,
strokeWidth,
outerAreaSize,
handleLineColor,
curveLineColor,
previewState
}) {
const { startCoordinate, endCoordinate, startBezierHandle, endBezierHandle } = bezierCurveParamsFromSizeAndValue(
size,
value
);
const svgWidth = size + strokeWidth * 2;
const svgHeight = size + strokeWidth * 2 + outerAreaSize * 2;
return /* @__PURE__ */ React2.createElement(
"svg",
{
className: BezierCurveEditor_default.curve,
fill: "none",
width: svgWidth,
height: svgHeight,
viewBox: `0 0 ${svgWidth} ${svgHeight}`
},
/* @__PURE__ */ React2.createElement("g", { transform: `translate(${strokeWidth}, ${outerAreaSize + strokeWidth})` }, /* @__PURE__ */ React2.createElement(
"line",
{
className: BezierCurveEditor_default.handleLine,
stroke: handleLineColor,
strokeWidth: "1",
strokeLinecap: "round",
x1: startCoordinate[0],
y1: startCoordinate[1],
x2: startBezierHandle[0],
y2: startBezierHandle[1]
}
), /* @__PURE__ */ React2.createElement(
"line",
{
className: BezierCurveEditor_default.handleLine,
stroke: handleLineColor,
strokeWidth: "1",
strokeLinecap: "round",
x1: endCoordinate[0],
y1: endCoordinate[1],
x2: endBezierHandle[0],
y2: endBezierHandle[1]
}
), /* @__PURE__ */ React2.createElement(
"path",
{
className: BezierCurveEditor_default.curveLine,
stroke: curveLineColor,
strokeWidth,
strokeLinecap: "round",
d: `M${startCoordinate} C${startBezierHandle} ${endBezierHandle} ${endCoordinate}`
}
), previewState !== "hidden" && /* @__PURE__ */ React2.createElement(
"circle",
{
className: BezierCurveEditor_default.preview,
r: 8,
cx: 0,
cy: 0,
strokeWidth,
style: {
offsetPath: `path('M${startCoordinate} C${startBezierHandle} ${endBezierHandle} ${endCoordinate}')`,
animationTimingFunction: `cubic-bezier(${value})`,
animationPlayState: previewState
}
}
))
);
}
// src/components/BezierCurveEditor/BezierCurveEditorEndPoints.tsx
var import_classnames = __toESM(require_classnames(), 1);
import React3 from "react";
function BezierCurveEditorEndPoints(_a) {
var _b = _a, {
size,
value,
outerAreaSize,
strokeWidth,
handleLineColor,
fixedHandleColor,
activeClassName
} = _b, props = __objRest(_b, [
"size",
"value",
"outerAreaSize",
"strokeWidth",
"handleLineColor",
"fixedHandleColor",
"activeClassName"
]);
const { startCoordinate, endCoordinate } = bezierCurveParamsFromSizeAndValue(size, value);
if (props.isEditable !== true) {
return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(
"span",
{
className: (0, import_classnames.default)(BezierCurveEditor_default.handle, BezierCurveEditor_default.fixed),
style: {
top: `${startCoordinate[1] + outerAreaSize + strokeWidth}px`,
left: `${startCoordinate[0] + strokeWidth}px`,
borderColor: handleLineColor,
backgroundColor: fixedHandleColor
}
}
), /* @__PURE__ */ React3.createElement(
"span",
{
className: (0, import_classnames.default)(BezierCurveEditor_default.handle, BezierCurveEditor_default.fixed),
style: {
top: `${endCoordinate[1] + outerAreaSize + strokeWidth}px`,
left: `${endCoordinate[0] + strokeWidth}px`,
borderColor: handleLineColor,
backgroundColor: fixedHandleColor
}
}
));
}
const {
movingEndPoint,
movingStartPoint,
handleEndPointPointerLeaveOrUp,
handleEndPointPointerMove,
handleEndPointStartMoving,
handleStartPointPointerLeaveOrUp,
handleStartPointPointerMove,
handleStartPointStartMoving
} = props;
return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(
"button",
{
type: "button",
className: (0, import_classnames.default)(BezierCurveEditor_default.handle, BezierCurveEditor_default.fixed, {
[BezierCurveEditor_default.active]: movingStartPoint,
[activeClassName]: !!activeClassName && movingStartPoint
}),
style: {
top: `${startCoordinate[1] + outerAreaSize + strokeWidth}px`,
left: `${startCoordinate[0] + strokeWidth}px`,
borderColor: handleLineColor,
backgroundColor: fixedHandleColor
},
onPointerDown: handleStartPointStartMoving,
onPointerMove: handleStartPointPointerMove,
onPointerUp: handleStartPointPointerLeaveOrUp
}
), /* @__PURE__ */ React3.createElement(
"button",
{
type: "button",
className: (0, import_classnames.default)(BezierCurveEditor_default.handle, BezierCurveEditor_default.fixed, {
[BezierCurveEditor_default.active]: movingEndPoint,
[activeClassName]: !!activeClassName && movingEndPoint
}),
style: {
top: `${endCoordinate[1] + outerAreaSize + strokeWidth}px`,
left: `${endCoordinate[0] + strokeWidth}px`,
borderColor: handleLineColor,
backgroundColor: fixedHandleColor
},
onPointerDown: handleEndPointStartMoving,
onPointerMove: handleEndPointPointerMove,
onPointerUp: handleEndPointPointerLeaveOrUp
}
));
}
// src/components/BezierCurveEditor/hooks.ts
import React4 from "react";
function useHandleState(moveHandle2, handleMoveStart) {
const handlePositionRef = React4.useRef({ x: 0, y: 0 });
const [movingHandle, setMovingHandle] = React4.useState(false);
const handlePointerStartMoving = React4.useCallback(
(event) => {
if (movingHandle)
return;
event.preventDefault();
setMovingHandle(true);
handleMoveStart();
const startX = event.clientX;
const startY = event.clientY;
handlePositionRef.current = { x: startX, y: startY };
event.currentTarget.setPointerCapture(event.pointerId);
},
[movingHandle, handleMoveStart]
);
const handlePointerMove = React4.useCallback(
(event) => {
if (movingHandle) {
const x = event.clientX;
const y = event.clientY;
moveHandle2(handlePositionRef.current, { x, y });
}
},
[moveHandle2, movingHandle]
);
const handlePointerLeaveOrUp = React4.useCallback(
(event) => {
event.currentTarget.releasePointerCapture(event.pointerId);
setMovingHandle(false);
},
[setMovingHandle]
);
return [movingHandle, { handlePointerLeaveOrUp, handlePointerMove, handlePointerStartMoving }];
}
// src/components/BezierCurveEditor/BezierCurveEditor.tsx
var defaultProps = {
size: 200,
outerAreaSize: 50,
strokeWidth: 2,
value: [0.4, 0, 1, 0.6]
// easeIn
};
function BezierCurveEditor(_a) {
var _b = _a, {
size = defaultProps.size,
strokeWidth = defaultProps.strokeWidth,
outerAreaSize = defaultProps.outerAreaSize,
value: valueProp = defaultProps.value,
innerAreaColor,
outerAreaColor,
rowColor,
handleLineColor,
curveLineColor,
fixedHandleColor,
startHandleColor,
endHandleColor,
enablePreview,
className,
startHandleClassName,
endHandleClassName,
fixedPointActiveClassName,
startHandleActiveClassName,
endHandleActiveClassName
} = _b, props = __objRest(_b, [
"size",
"strokeWidth",
"outerAreaSize",
"value",
"innerAreaColor",
"outerAreaColor",
"rowColor",
"handleLineColor",
"curveLineColor",
"fixedHandleColor",
"startHandleColor",
"endHandleColor",
"enablePreview",
"className",
"startHandleClassName",
"endHandleClassName",
"fixedPointActiveClassName",
"startHandleActiveClassName",
"endHandleActiveClassName"
]);
const initialValueRef = React5.useRef(
valueProp.length === 4 ? [0, 0, ...valueProp, 1, 1] : valueProp
);
const value = React5.useMemo(() => {
return valueProp.length === 4 ? [0, 0, ...valueProp, 1, 1] : valueProp;
}, [valueProp]);
const updateValueRef = React5.useCallback(() => {
initialValueRef.current = [...value];
}, [value]);
const moveStartHandle = React5.useCallback(
(start, movement) => {
const nextValue = moveHandle(initialValueRef.current, size, [2, 3], start, movement);
const clampedValue = clampValue(outerAreaSize, size, nextValue);
if (props.onChange && props.allowNodeEditing)
props.onChange(clampedValue);
if (props.onChange && props.allowNodeEditing !== true)
props.onChange(clampedValue.slice(2, 6));
},
[size, outerAreaSize, props.onChange, props.allowNodeEditing]
);
const [
movingStartHandle,
{
handlePointerLeaveOrUp: handleStartHandlePointerLeaveOrUp,
handlePointerMove: handleStartHandlePointerMove,
handlePointerStartMoving: handleStartHandleStartMoving
}
] = useHandleState(moveStartHandle, updateValueRef);
const moveEndHandle = React5.useCallback(
(start, movement) => {
const nextValue = moveHandle(initialValueRef.current, size, [4, 5], start, movement);
const clampedValue = clampValue(outerAreaSize, size, nextValue);
if (props.onChange && props.allowNodeEditing)
props.onChange(clampedValue);
if (props.onChange && props.allowNodeEditing !== true)
props.onChange(clampedValue.slice(2, 6));
},
[size, outerAreaSize, props.onChange, props.allowNodeEditing]
);
const [
movingEndHandle,
{
handlePointerLeaveOrUp: handleEndHandlePointerLeaveOrUp,
handlePointerMove: handleEndHandlePointerMove,
handlePointerStartMoving: handleEndHandleStartMoving
}
] = useHandleState(moveEndHandle, updateValueRef);
const moveStartPoint = React5.useCallback(
(start, movement) => {
const nextValue = moveHandle(initialValueRef.current, size, [0, 1], start, movement);
const clampedValue = clampValue(outerAreaSize, size, nextValue);
if (props.onChange && props.allowNodeEditing)
props.onChange(clampedValue);
if (props.onChange && props.allowNodeEditing !== true)
props.onChange(clampedValue.slice(2, 6));
},
[size, outerAreaSize, props.onChange, props.allowNodeEditing]
);
const [
movingStartPoint,
{
handlePointerLeaveOrUp: handleStartPointPointerLeaveOrUp,
handlePointerMove: handleStartPointPointerMove,
handlePointerStartMoving: handleStartPointStartMoving
}
] = useHandleState(moveStartPoint, updateValueRef);
const moveEndPoint = React5.useCallback(
(start, movement) => {
const nextValue = moveHandle(initialValueRef.current, size, [6, 7], start, movement);
const clampedValue = clampValue(outerAreaSize, size, nextValue);
if (props.onChange && props.allowNodeEditing)
props.onChange(clampedValue);
if (props.onChange && props.allowNodeEditing !== true)
props.onChange(clampedValue.slice(2, 6));
},
[size, outerAreaSize, props.onChange, props.allowNodeEditing]
);
const [
movingEndPoint,
{
handlePointerLeaveOrUp: handleEndPointPointerLeaveOrUp,
handlePointerMove: handleEndPointPointerMove,
handlePointerStartMoving: handleEndPointStartMoving
}
] = useHandleState(moveEndPoint, updateValueRef);
const { startBezierHandle, endBezierHandle } = bezierCurveParamsFromSizeAndValue(size, value);
return /* @__PURE__ */ React5.createElement("div", { className: (0, import_classnames2.default)(BezierCurveEditor_default.root, className) }, /* @__PURE__ */ React5.createElement("div", { className: BezierCurveEditor_default.wrap }, /* @__PURE__ */ React5.createElement(
"div",
{
role: "presentation",
className: BezierCurveEditor_default.bg,
style: {
left: `${strokeWidth}px`,
width: `${size - strokeWidth}px`,
backgroundColor: outerAreaColor
}
}
), /* @__PURE__ */ React5.createElement(
BezierCurveEditorPlane,
{
size,
outerAreaSize,
strokeWidth,
innerAreaColor,
rowColor
}
), /* @__PURE__ */ React5.createElement(
BezierCurveEditorCurve,
{
value,
size,
strokeWidth,
outerAreaSize,
handleLineColor,
curveLineColor,
previewState: !enablePreview ? "hidden" : movingStartHandle || movingEndHandle ? "paused" : "running"
}
), /* @__PURE__ */ React5.createElement(
BezierCurveEditorEndPoints,
{
size,
value,
outerAreaSize,
strokeWidth,
handleLineColor,
fixedHandleColor,
isEditable: props.allowNodeEditing,
activeClassName: fixedPointActiveClassName,
movingEndPoint,
movingStartPoint,
handleStartPointStartMoving,
handleStartPointPointerMove,
handleStartPointPointerLeaveOrUp,
handleEndPointStartMoving,
handleEndPointPointerMove,
handleEndPointPointerLeaveOrUp
}
), /* @__PURE__ */ React5.createElement(
"button",
{
type: "button",
className: (0, import_classnames2.default)(BezierCurveEditor_default.handle, BezierCurveEditor_default.start, startHandleClassName, {
[BezierCurveEditor_default.active]: movingStartHandle,
[startHandleActiveClassName]: !!startHandleActiveClassName && movingStartHandle
}),
style: {
top: `${startBezierHandle[1] + outerAreaSize + strokeWidth}px`,
left: `${startBezierHandle[0] + strokeWidth}px`,
color: startHandleColor,
backgroundColor: startHandleColor
},
onPointerDown: handleStartHandleStartMoving,
onPointerMove: handleStartHandlePointerMove,
onPointerUp: handleStartHandlePointerLeaveOrUp
}
), /* @__PURE__ */ React5.createElement(
"button",
{
type: "button",
className: (0, import_classnames2.default)(BezierCurveEditor_default.handle, BezierCurveEditor_default.end, endHandleClassName, {
[BezierCurveEditor_default.active]: movingEndHandle,
[endHandleActiveClassName]: !!endHandleActiveClassName && movingEndHandle
}),
style: {
top: `${endBezierHandle[1] + outerAreaSize + strokeWidth}px`,
left: `${endBezierHandle[0] + strokeWidth}px`,
color: endHandleColor,
backgroundColor: endHandleColor
},
onPointerDown: handleEndHandleStartMoving,
onPointerMove: handleEndHandlePointerMove,
onPointerUp: handleEndHandlePointerLeaveOrUp
}
)));
}
function clampValue(outerAreaSize, size, value) {
const nextValue = [...value];
const allowedOuterValue = outerAreaSize / size;
nextValue[0] = Math.max(0, Math.min(1, nextValue[0]));
nextValue[1] = Math.max(0, Math.min(1, nextValue[1]));
nextValue[2] = Math.max(0, Math.min(1, nextValue[2]));
nextValue[4] = Math.max(0, Math.min(1, nextValue[4]));
nextValue[6] = Math.max(0, Math.min(1, nextValue[6]));
nextValue[7] = Math.max(0, Math.min(1, nextValue[7]));
nextValue[3] = Math.max(-allowedOuterValue, Math.min(1 + allowedOuterValue, nextValue[3]));
nextValue[5] = Math.max(-allowedOuterValue, Math.min(1 + allowedOuterValue, nextValue[5]));
return nextValue;
}
function moveHandle(value, size, targetIndices, start, { x, y }) {
const nextValue = [...value];
const [xIndex, yIndex] = targetIndices;
const relXMoved = (x - start.x) / size;
const relYMoved = (y - start.y) / size;
nextValue[xIndex] = nextValue[xIndex] + relXMoved;
nextValue[yIndex] = nextValue[yIndex] - relYMoved;
return nextValue;
}
export {
BezierCurveEditor,
bezierCurveParamsFromSizeAndValue
};
/*! Bundled license information:
classnames/index.js:
(*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*)
*/