grungnet/src/helpers/konva.js
2021-01-02 18:01:16 +11:00

260 lines
6.8 KiB
JavaScript

import React, { useState, useEffect, useRef } from "react";
import { Line, Group, Path, Circle } from "react-konva";
import { lerp } from "./shared";
import * as Vector2 from "./vector2";
// Holes should be wound in the opposite direction as the containing points array
export function HoleyLine({ holes, ...props }) {
// Converted from https://github.com/rfestag/konva/blob/master/src/shapes/Line.ts
function drawLine(points, context, shape) {
const length = points.length;
const tension = shape.tension();
const closed = shape.closed();
const bezier = shape.bezier();
if (!length) {
return;
}
context.moveTo(points[0], points[1]);
if (tension !== 0 && length > 4) {
const tensionPoints = shape.getTensionPoints();
const tensionLength = tensionPoints.length;
let n = closed ? 0 : 4;
if (!closed) {
context.quadraticCurveTo(
tensionPoints[0],
tensionPoints[1],
tensionPoints[2],
tensionPoints[3]
);
}
while (n < tensionLength - 2) {
context.bezierCurveTo(
tensionPoints[n++],
tensionPoints[n++],
tensionPoints[n++],
tensionPoints[n++],
tensionPoints[n++],
tensionPoints[n++]
);
}
if (!closed) {
context.quadraticCurveTo(
tensionPoints[tensionLength - 2],
tensionPoints[tensionLength - 1],
points[length - 2],
points[length - 1]
);
}
} else if (bezier) {
// no tension but bezier
let n = 2;
while (n < length) {
context.bezierCurveTo(
points[n++],
points[n++],
points[n++],
points[n++],
points[n++],
points[n++]
);
}
} else {
// no tension
for (let n = 2; n < length; n += 2) {
context.lineTo(points[n], points[n + 1]);
}
}
}
// Draw points and holes
function sceneFunc(context, shape) {
const points = shape.points();
const closed = shape.closed();
if (!points.length) {
return;
}
context.beginPath();
drawLine(points, context, shape);
context.beginPath();
drawLine(points, context, shape);
// closed e.g. polygons and blobs
if (closed) {
context.closePath();
if (holes && holes.length) {
for (let hole of holes) {
drawLine(hole, context, shape);
context.closePath();
}
}
context.fillStrokeShape(shape);
} else {
// open e.g. lines and splines
context.strokeShape(shape);
}
}
return <Line sceneFunc={sceneFunc} {...props} />;
}
export function Tick({ x, y, scale, onClick, cross }) {
const [fill, setFill] = useState("white");
function handleEnter() {
setFill("hsl(260, 100%, 80%)");
}
function handleLeave() {
setFill("white");
}
return (
<Group
x={x}
y={y}
scaleX={scale}
scaleY={scale}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
onClick={onClick}
onTap={onClick}
>
<Circle radius={12} fill="hsla(230, 25%, 18%, 0.8)" />
<Path
offsetX={12}
offsetY={12}
fill={fill}
data={
cross
? "M18.3 5.71c-.39-.39-1.02-.39-1.41 0L12 10.59 7.11 5.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L10.59 12 5.7 16.89c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L12 13.41l4.89 4.89c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z"
: "M9 16.2l-3.5-3.5c-.39-.39-1.01-.39-1.4 0-.39.39-.39 1.01 0 1.4l4.19 4.19c.39.39 1.02.39 1.41 0L20.3 7.7c.39-.39.39-1.01 0-1.4-.39-.39-1.01-.39-1.4 0L9 16.2z"
}
/>
</Group>
);
}
export function Trail({ position, size, duration, segments }) {
const trailRef = useRef();
const pointsRef = useRef([]);
const prevPositionRef = useRef(position);
// Add a new point every time position is changed
useEffect(() => {
if (Vector2.compare(position, prevPositionRef.current, 0.0001)) {
return;
}
pointsRef.current.push({ ...position, lifetime: duration });
prevPositionRef.current = position;
}, [position, duration]);
// Advance lifetime of trail
useEffect(() => {
let prevTime = performance.now();
let request = requestAnimationFrame(animate);
function animate(time) {
request = requestAnimationFrame(animate);
const deltaTime = time - prevTime;
prevTime = time;
if (pointsRef.current.length === 0) {
return;
}
let expired = 0;
for (let point of pointsRef.current) {
point.lifetime -= deltaTime;
if (point.lifetime < 0) {
expired++;
}
}
if (expired > 0) {
pointsRef.current = pointsRef.current.slice(expired);
}
if (trailRef.current) {
trailRef.current.getLayer().draw();
}
}
return () => {
cancelAnimationFrame(request);
};
}, []);
// Custom scene function for drawing a trail from a line
function sceneFunc(context) {
// Resample points to ensure a smooth trail
const resampledPoints = Vector2.resample(pointsRef.current, segments);
for (let i = 1; i < resampledPoints.length; i++) {
const from = resampledPoints[i - 1];
const to = resampledPoints[i];
const alpha = i / resampledPoints.length;
context.beginPath();
context.lineJoin = "round";
context.lineCap = "round";
context.lineWidth = alpha * size;
context.strokeStyle = `hsl(0, 63%, ${lerp(90, 50, alpha)}%)`;
context.moveTo(from.x, from.y);
context.lineTo(to.x, to.y);
context.stroke();
context.closePath();
}
}
return (
<Group>
<Line sceneFunc={sceneFunc} ref={trailRef} />
<Circle
x={position.x}
y={position.y}
fill="hsl(0, 63%, 50%)"
width={size}
height={size}
/>
</Group>
);
}
Trail.defaultProps = {
// Duration of each point in milliseconds
duration: 200,
// Number of segments in the trail, resampled from the points
segments: 20,
};
export function getRelativePointerPosition(node) {
let transform = node.getAbsoluteTransform().copy();
transform.invert();
let posision = node.getStage().getPointerPosition();
return transform.point(posision);
}
export function getRelativePointerPositionNormalized(node) {
const relativePosition = getRelativePointerPosition(node);
return {
x: relativePosition.x / node.width(),
y: relativePosition.y / node.height(),
};
}
/**
* Converts points from alternating array form to vector array form
* @param {number[]} points points in an x, y alternating array
* @returns {Vector2[]} a `Vector2` array
*/
export function convertPointArray(points) {
return points.reduce((acc, _, i, arr) => {
if (i % 2 === 0) {
acc.push({ x: arr[i], y: arr[i + 1] });
}
return acc;
}, []);
}