Skip to content

Commit

Permalink
feat: animation of color
Browse files Browse the repository at this point in the history
  • Loading branch information
sheepbox8646 committed Nov 30, 2024
1 parent 9d3dd63 commit 044fbf0
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 41 deletions.
1 change: 1 addition & 0 deletions packages/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
},
"dependencies": {
"@vue-motion/core": "workspace:*",
"@vue-motion/utils": "workspace:*",
"vue": "^3.5.11"
},
"devDependencies": {
Expand Down
157 changes: 123 additions & 34 deletions packages/lib/src/animations/color.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineAnimation } from "@vue-motion/core";
import { findColor, colorTable } from "@vue-motion/utils";

export interface Colorable {
fillColor?: string;
Expand All @@ -11,6 +12,90 @@ export interface colorableMixin {
discolorateTo: (on: "fill" | "border" | "color", to: string) => void;
}

interface RGB {
r: number;
g: number;
b: number;
a?: number;
}

function hexToRgb(color: string): RGB {
if (!/^[#rgb]/.test(color)) {
const namedColor = findColor(color as keyof typeof colorTable);
if (namedColor) {
color = namedColor;
}
}

if (color.startsWith("rgb")) {
const values = color.match(/\d+(\.\d+)?/g);
if (!values) return { r: 0, g: 0, b: 0 };
return {
r: parseInt(values[0]),
g: parseInt(values[1]),
b: parseInt(values[2]),
...(values[3] ? { a: parseFloat(values[3]) } : {}),
};
}

const hex = color.replace(/^#/, "");
const bigint = parseInt(hex, 16);
return {
r: (bigint >> 16) & 255,
g: (bigint >> 8) & 255,
b: bigint & 255,
};
}

function rgbToString(rgb: RGB): string {
if (typeof rgb.a !== "undefined") {
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
}

return (
"#" +
[rgb.r, rgb.g, rgb.b]
.map((x) => Math.round(x).toString(16).padStart(2, "0"))
.join("")
);
}

function interpolateColor(from: string, to: string, progress: number): string {
const fromRgb = hexToRgb(from);
const toRgb = hexToRgb(to);

const result: RGB = {
r: Math.round(fromRgb.r + (toRgb.r - fromRgb.r) * progress),
g: Math.round(fromRgb.g + (toRgb.g - fromRgb.g) * progress),
b: Math.round(fromRgb.b + (toRgb.b - fromRgb.b) * progress),
};

if (typeof fromRgb.a !== "undefined" || typeof toRgb.a !== "undefined") {
const fromAlpha = fromRgb.a ?? 1;
const toAlpha = toRgb.a ?? 1;
result.a = fromAlpha + (toAlpha - fromAlpha) * progress;
}

return rgbToString(result);
}

function shiftColor(color: string, offset: string): string {
const rgb = hexToRgb(color);
const offsetRgb = hexToRgb(offset);

const result: RGB = {
r: Math.min(255, Math.max(0, rgb.r + offsetRgb.r)),
g: Math.min(255, Math.max(0, rgb.g + offsetRgb.g)),
b: Math.min(255, Math.max(0, rgb.b + offsetRgb.b)),
};

if (typeof rgb.a !== "undefined") {
result.a = rgb.a;
}

return rgbToString(result);
}

export const discolorate = defineAnimation<
Colorable,
{
Expand All @@ -19,42 +104,46 @@ export const discolorate = defineAnimation<
on: "fill" | "border" | "color";
}
>((target, context) => {
function resolve(color: string) {
// return the r, g, b, a?'s value, the format can be #rgb, #rgba, #rrggbb, #rrggbbaa, rgb(), rgba()
const match = color.match(
/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i,
);
if (match) {
return match.slice(1).map((hex) => Number.parseInt(hex, 16));
}
return null;
const propertyMap = {
fill: "fillColor",
border: "borderColor",
color: "color",
};
const property = propertyMap[context.on] as keyof Colorable;

const fromColor = context.from || target[property] || "#000000";
const toColor = context.offset
? shiftColor(fromColor, context.offset)
: target[property] || "#000000";

return (progress) => {
target[property] = interpolateColor(fromColor, toColor, progress);
};
});

export const discolorateTo = defineAnimation<
Colorable,
{
to: string;
on: "fill" | "border" | "color";
}
const parsedFrom = resolve(
context.from ??
(context.on === "fill"
? (target.fillColor ?? "#000000")
: context.on === "border"
? (target.borderColor ?? "#000000")
: (target.color ?? "#000000")),
);
if (!parsedFrom) return () => {};

const parsedOffset = resolve(
context.offset ??
(context.on === "fill"
? (target.fillColor ?? "#000000")
: context.on === "border"
? (target.borderColor ?? "#000000")
: (target.color ?? "#000000")),
);
if (!parsedOffset) return () => {};
>((target, context) => {
const propertyMap = {
fill: "fillColor",
border: "borderColor",
color: "color",
};
const property = propertyMap[context.on] as keyof Colorable;

const currentColor = target[property];
const fromColor = currentColor
? findColor(currentColor as keyof typeof colorTable) || currentColor
: "#000000";

const toColor =
findColor(context.to as keyof typeof colorTable) || context.to;

return (progress) => {
const offset = parsedOffset.map((value, _index) => value * progress);
const result = parsedFrom.map((value, index) => value + offset[index]);
if (context.on === "fill") target.fillColor = `rgba(${result.join(",")})`;
else if (context.on === "border")
target.borderColor = `rgba(${result.join(",")})`;
else target.color = `rgba(${result.join(",")})`;
target[property] = interpolateColor(fromColor, toColor, progress);
};
});
4 changes: 2 additions & 2 deletions packages/lib/src/animations/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface StrokableMixin extends Strokable {
traceFill: (origin: number, options?: AnimationParams) => void;
}

export const stroke = defineAnimation<
export const trace = defineAnimation<
Strokable,
{
origin?: number;
Expand All @@ -23,7 +23,7 @@ export const stroke = defineAnimation<
] as number[];
});

export const strokeFill = defineAnimation<
export const traceFill = defineAnimation<
Strokable & HasOpacity,
{
origin?: number;
Expand Down
14 changes: 13 additions & 1 deletion packages/lib/src/widgets/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
zoomTo,
} from "../animations";
import type { Colorable, colorableMixin } from "../animations/color";
import { discolorate } from "../animations/color";
import { discolorate, discolorateTo } from "../animations/color";
import type {
Animation,
AnimationInstance,
Expand Down Expand Up @@ -264,6 +264,18 @@ export function widget(options: WidgetOptions) {
});
},
);
registerAnimation<Colorable>(
"discolorateTo",
(on: "fill" | "border" | "color", to: string, params?: AnimationParams) => {
return (manager) =>
manager.animate(discolorateTo, {
to,
on,
duration: params?.duration ?? defaultDuration,
by: params?.by ?? ((x) => x),
});
},
);
registerAnimation<Positional>(
"moveOnFunction",
(
Expand Down
154 changes: 154 additions & 0 deletions packages/utils/src/color-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
export const colorTable = {
// 红色系
red: "#ff0000",
crimson: "#dc143c",
darkred: "#8b0000",
firebrick: "#b22222",
indianred: "#cd5c5c",
lightcoral: "#f08080",
maroon: "#800000",
salmon: "#fa8072",
darksalmon: "#e9967a",
lightsalmon: "#ffa07a",

// 粉色系
pink: "#ffc0cb",
deeppink: "#ff1493",
hotpink: "#ff69b4",
lightpink: "#ffb6c1",
palevioletred: "#db7093",

// 橙色系
orange: "#ffa500",
darkorange: "#ff8c00",
coral: "#ff7f50",
tomato: "#ff6347",
orangered: "#ff4500",

// 黄色系
yellow: "#ffff00",
gold: "#ffd700",
goldenrod: "#daa520",
darkgoldenrod: "#b8860b",
khaki: "#f0e68c",
darkkhaki: "#bdb76b",
palegoldenrod: "#eee8aa",
lightyellow: "#ffffe0",

// 绿色系
green: "#008000",
lime: "#00ff00",
limegreen: "#32cd32",
forestgreen: "#228b22",
darkgreen: "#006400",
greenyellow: "#adff2f",
yellowgreen: "#9acd32",
springgreen: "#00ff7f",
mediumspringgreen: "#00fa9a",
lightgreen: "#90ee90",
palegreen: "#98fb98",
darkseagreen: "#8fbc8f",
seagreen: "#2e8b57",
olive: "#808000",
darkolivegreen: "#556b2f",
olivedrab: "#6b8e23",

// 青色系
cyan: "#00ffff",
aqua: "#00ffff",
aquamarine: "#7fffd4",
mediumaquamarine: "#66cdaa",
turquoise: "#40e0d0",
darkturquoise: "#00ced1",
lightseagreen: "#20b2aa",
mediumturquoise: "#48d1cc",
paleturquoise: "#afeeee",
teal: "#008080",

// 蓝色系
blue: "#0000ff",
darkblue: "#00008b",
mediumblue: "#0000cd",
navy: "#000080",
midnightblue: "#191970",
royalblue: "#4169e1",
cornflowerblue: "#6495ed",
dodgerblue: "#1e90ff",
deepskyblue: "#00bfff",
lightskyblue: "#87cefa",
skyblue: "#87ceeb",
lightblue: "#add8e6",
powderblue: "#b0e0e6",
steelblue: "#4682b4",
lightsteelblue: "#b0c4de",
cadetblue: "#5f9ea0",

// 紫色系
purple: "#800080",
magenta: "#ff00ff",
fuchsia: "#ff00ff",
darkmagenta: "#8b008b",
darkorchid: "#9932cc",
darkviolet: "#9400d3",
blueviolet: "#8a2be2",
mediumpurple: "#9370db",
mediumorchid: "#ba55d3",
orchid: "#da70d6",
violet: "#ee82ee",
plum: "#dda0dd",
thistle: "#d8bfd8",
lavender: "#e6e6fa",
indigo: "#4b0082",
slateblue: "#6a5acd",
mediumslateblue: "#7b68ee",
darkslateblue: "#483d8b",

// 棕色系
brown: "#a52a2a",
saddlebrown: "#8b4513",
sienna: "#a0522d",
chocolate: "#d2691e",
peru: "#cd853f",
sandybrown: "#f4a460",
rosybrown: "#bc8f8f",
tan: "#d2b48c",
burlywood: "#deb887",
wheat: "#f5deb3",

// 白色系
white: "#ffffff",
snow: "#fffafa",
honeydew: "#f0fff0",
mintcream: "#f5fffa",
azure: "#f0ffff",
aliceblue: "#f0f8ff",
ghostwhite: "#f8f8ff",
whitesmoke: "#f5f5f5",
seashell: "#fff5ee",
beige: "#f5f5dc",
oldlace: "#fdf5e6",
floralwhite: "#fffaf0",
ivory: "#fffff0",
antiquewhite: "#faebd7",
linen: "#faf0e6",
lavenderblush: "#fff0f5",
mistyrose: "#ffe4e1",

// 灰色系
gainsboro: "#dcdcdc",
lightgray: "#d3d3d3",
silver: "#c0c0c0",
darkgray: "#a9a9a9",
gray: "#808080",
dimgray: "#696969",
lightslategray: "#778899",
slategray: "#708090",
darkslategray: "#2f4f4f",
black: "#000000",
} as const;

export function findColor(name: keyof typeof colorTable): string {
return colorTable[name] || "";
}

export default colorTable;
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./deep-clone";
export * from "./judgements";
export * from "./color-table";
Loading

0 comments on commit 044fbf0

Please sign in to comment.