zeplin-extension-style-kit
Version:
Models and utilities to generate CSS-like style code in Zeplin extensions.
162 lines (133 loc) • 4.87 kB
text/typescript
import { ColorStop } from "@zeplin/extension-model";
import { ColorNameResolver, ColorParams, StyleValue } from "../../common.js";
import { LinearColorStop } from "./linearColorStop.js";
interface Point {
x: number;
y: number;
}
export class LinearGradient implements StyleValue {
angle: number;
colorStops: LinearColorStop[];
constructor(
colorStops: ColorStop[],
from: Point,
to: Point,
width: number,
height: number
) {
/*
* Convert the relative `from` and `to` positions on a layer to absolute positions
* by multiplying them with the width and height of the layer
*/
const x1 = from.x * width;
const y1 = from.y * height;
const x2 = to.x * width;
const y2 = to.y * height;
this.angle = (360 - Math.round(Math.atan2(x1 - x2, y1 - y2) * 180 / Math.PI)) % 360;
let pFirst: number;
let pLast: number;
let pStart: number;
let pEnd: number;
// Find the slope of the gradient line
const m1 = (y2 - y1) / (x2 - x1);
if (m1 === 0) {
// If the slope is 0, gradient line is horizontal
pFirst = x1;
pLast = x2;
pStart = 0;
pEnd = width;
} else if (!Number.isFinite(m1)) {
// If the slope is infinite, gradient line is vertical
pFirst = y1;
pLast = y2;
pStart = 0;
pEnd = height;
} else {
pFirst = x1;
pLast = x2;
// Find c1 such that y = m1 * x + c1, by applying one of the positions
const c1 = y1 - m1 * x1;
// The slope of a perpendicular line
const m2 = -1 / m1;
// Starting and ending points
let x0;
let y0;
let xe;
let ye;
if (m1 > 0) {
/*
* If the slope is positive, starting point is top-left corner (0, 0)
* and ending point is bottom-right corner (w, h)
*/
x0 = 0;
y0 = 0;
xe = width;
ye = height;
} else {
/*
* If the slope is negative, starting point is bottom-left corner (0, h)
* and ending point is top-right corner (w, 0)
*/
x0 = 0;
y0 = height;
xe = width;
ye = 0;
}
/*
* `c2` equation constant of the line crossing from starting point
* and perpendicular to the gradient line
*/
const c2 = y0 - m2 * x0;
/*
* `x` position of the intersection point of gradient line
* and the perpendecular line crossing from starting point
*/
pStart = (c2 - c1) / (m1 - m2);
/*
* `c3` equation constant of the line crossing from ending point
* and perpendicular to the gradient line
*/
const c3 = ye - m2 * xe;
/*
* `x` position of the intersection point of gradient line
* and the perpendecular line crossing from ending point
*/
pEnd = (c3 - c1) / (m1 - m2);
}
this.colorStops = colorStops.map(({ color, position: p }) => {
// Convert the position coming from design tool to position on the actual gradient line
const position = (p * (pLast - pFirst) + pFirst - pStart) / (pEnd - pStart);
return new LinearColorStop({ color, position });
});
}
valueOf(): string {
const { angle, colorStops } = this;
return `linearGradient::a:${angle}:${colorStops.map(cs => cs.valueOf()).join(":")}`;
}
equals(other: LinearGradient): boolean {
return (
this.angle === other.angle &&
this.colorStops.length === other.colorStops.length &&
this.colorStops.every((cs, index) => cs.equals(other.colorStops[index]))
);
}
toCSSAngle(angle: number) {
switch (angle) {
case 0:
return "to top";
case 90:
return "to right";
case 180:
return "to bottom";
case 270:
return "to left";
default:
return `${angle}deg`;
}
}
toStyleValue({ colorFormat }: ColorParams, colorNameResolver: ColorNameResolver) {
const { angle, colorStops } = this;
const colorStopStyle = colorStops.map(cs => cs.toStyleValue({ colorFormat }, colorNameResolver)).join(", ");
return `linear-gradient(${this.toCSSAngle(angle)}, ${colorStopStyle})`;
}
}