import { splitBracket, splitComma, convertUnitSize, splitSpace, splitUnit } from "@daybrush/utils"; import { minus } from "@scena/matrix"; import { abs, convertCSSSize } from "../../utils"; import { getRadiusStyles, getRadiusValues } from "../roundable/borderRadius"; import { MoveableManagerInterface, ClippableProps, ControlPose } from "../../types"; import { getMinMaxs } from "overlap-area"; import { getCachedStyle } from "../../store/Store"; export const CLIP_DIRECTIONS = [ [0, -1, "n"], [1, 0, "e"], ] as const; export const CLIP_RECT_DIRECTIONS = [ [-1, -1, "nw"], [0, -1, "n"], [1, -1, "ne"], [1, 0, "e"], [1, 1, "se"], [0, 1, "s"], [-1, 1, "sw"], [-1, 0, "w"], ] as const; // 1 2 5 6 0 3 4 7 // 0 1 2 3 4 5 6 7 export function getClipStyles( moveable: MoveableManagerInterface, clipPath: ReturnType, poses: number[][], ) { const { clipRelative, } = moveable.props; const { width, height, } = moveable.state; const { type: clipType, poses: clipPoses, } = clipPath!; const isRect = clipType === "rect"; const isCircle = clipType === "circle"; if (clipType === "polygon") { return poses.map(pos => `${convertCSSSize(pos[0], width, clipRelative)} ${convertCSSSize(pos[1], height, clipRelative)}`); } else if (isRect || clipType === "inset") { const top = poses[1][1]; const right = poses[3][0]; const left = poses[7][0]; const bottom = poses[5][1]; if (isRect) { return [ top, right, bottom, left, ].map(pos => `${pos}px`); } const clipStyles = [top, width - right, height - bottom, left] .map((pos, i) => convertCSSSize(pos, i % 2 ? width : height, clipRelative)); if (poses.length > 8) { const [subWidth, subHeight] = minus(poses[4], poses[0]); clipStyles.push("round", ...getRadiusStyles( clipPoses.slice(8).map((info, i) => { return { ...info, pos: poses[i], }; }), clipRelative!, subWidth, subHeight, left, top, right, bottom, ).styles); } return clipStyles; } else if (isCircle || clipType === "ellipse") { const center = poses[0]; const ry = convertCSSSize( abs(poses[1][1] - center[1]), isCircle ? Math.sqrt((width * width + height * height) / 2) : height, clipRelative, ); const clipStyles = isCircle ? [ry] : [convertCSSSize(abs(poses[2][0] - center[0]), width, clipRelative), ry]; clipStyles.push( "at", convertCSSSize(center[0], width, clipRelative), convertCSSSize(center[1], height, clipRelative)); return clipStyles; } } export function getRectPoses(top: number, right: number, bottom: number, left: number): ControlPose[] { const xs = [left, (left + right) / 2, right]; const ys = [top, (top + bottom) / 2, bottom]; return CLIP_RECT_DIRECTIONS.map(([dirx, diry, dir]) => { const x = xs[dirx + 1]; const y = ys[diry + 1]; return { vertical: abs(diry), horizontal: abs(dirx), direction: dir, pos: [x, y], }; }); } export function getControlSize( controlPoses: ControlPose[], ) { const xRange = [Infinity, -Infinity]; const yRange = [Infinity, -Infinity]; controlPoses.forEach(({ pos }) => { xRange[0] = Math.min(xRange[0], pos[0]); xRange[1] = Math.max(xRange[1], pos[0]); yRange[0] = Math.min(yRange[0], pos[1]); yRange[1] = Math.max(yRange[1], pos[1]); }); return [ abs(xRange[1] - xRange[0]), abs(yRange[1] - yRange[0]), ]; } export function getClipPath( target: HTMLElement | SVGElement | undefined | null, width: number, height: number, defaultClip?: string, customClip?: string, ) { if (!target) { return; } let clipText: string | undefined = customClip; if (!clipText) { const getStyle = getCachedStyle(target!); const clipPath = getStyle("clipPath"); clipText = clipPath !== "none" ? clipPath : getStyle("clip"); } if (!clipText || clipText === "none" || clipText === "auto") { clipText = defaultClip; if (!clipText) { return; } } const { prefix: clipPrefix = clipText, value = "", } = splitBracket(clipText); const isCircle = clipPrefix === "circle"; let splitter = " "; if (clipPrefix === "polygon") { const values = splitComma(value! || `0% 0%, 100% 0%, 100% 100%, 0% 100%`); splitter = ","; const poses: ControlPose[] = values.map(pos => { const [xPos, yPos] = pos.split(" "); return { vertical: 1, horizontal: 1, pos: [ convertUnitSize(xPos, width), convertUnitSize(yPos, height), ], }; }); const minMaxs = getMinMaxs(poses.map(pos => pos.pos)); return { type: clipPrefix, clipText, poses, splitter, left: minMaxs.minX, right: minMaxs.maxX, top: minMaxs.minY, bottom: minMaxs.maxY, } as const; } else if (isCircle || clipPrefix === "ellipse") { let xPos = ""; let yPos = ""; let radiusX = 0; let radiusY = 0; const values = splitSpace(value!); if (isCircle) { let radius = ""; [radius = "50%", , xPos = "50%", yPos = "50%"] = values; radiusX = convertUnitSize(radius, Math.sqrt((width * width + height * height) / 2)); radiusY = radiusX; } else { let xRadius = ""; let yRadius = ""; [xRadius = "50%", yRadius = "50%", , xPos = "50%", yPos = "50%"] = values; radiusX = convertUnitSize(xRadius, width); radiusY = convertUnitSize(yRadius, height); } const centerPos = [ convertUnitSize(xPos, width), convertUnitSize(yPos, height), ]; const poses: ControlPose[] = [ { vertical: 1, horizontal: 1, pos: centerPos, direction: "nesw", }, ...CLIP_DIRECTIONS.slice(0, isCircle ? 1 : 2).map(dir => ({ vertical: abs(dir[1]), horizontal: dir[0], direction: dir[2], sub: true, pos: [ centerPos[0] + dir[0] * radiusX, centerPos[1] + dir[1] * radiusY, ], })), ]; return { type: clipPrefix as "circle" | "ellipse", clipText, radiusX, radiusY, left: centerPos[0] - radiusX, top: centerPos[1] - radiusY, right: centerPos[0] + radiusX, bottom: centerPos[1] + radiusY, poses, splitter, } as const; } else if (clipPrefix === "inset") { const values = splitSpace(value! || "0 0 0 0"); const roundIndex = values.indexOf("round"); const rectLength = (roundIndex > -1 ? values.slice(0, roundIndex) : values).length; const radiusValues = values.slice(rectLength + 1); const [ topValue, rightValue = topValue, bottomValue = topValue, leftValue = rightValue, ] = values.slice(0, rectLength); const [top, bottom] = [topValue, bottomValue].map(pos => convertUnitSize(pos, height)); const [left, right] = [leftValue, rightValue].map(pos => convertUnitSize(pos, width)); const nextRight = width - right; const nextBottom = height - bottom; const radiusPoses = getRadiusValues( radiusValues, nextRight - left, nextBottom - top, left, top, ); const poses: ControlPose[] = [ ...getRectPoses(top, nextRight, nextBottom, left), ...radiusPoses, ]; return { type: "inset", clipText, poses, top, left, right: nextRight, bottom: nextBottom, radius: radiusValues, splitter, } as const; } else if (clipPrefix === "rect") { // top right bottom left const values = splitComma(value! || `0px, ${width}px, ${height}px, 0px`); splitter = ","; const [top, right, bottom, left] = values.map(pos => { const { value: posValue } = splitUnit(pos); return posValue; }); const poses = getRectPoses(top, right, bottom, left); return { type: "rect", clipText, poses, top, right, bottom, left, values, splitter, } as const; } return; }