ng-cw-v12
Version:
Angular UI Component Library
445 lines (440 loc) • 21 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, Directive, Input, Output, NgModule } from '@angular/core';
class ElectricBorderDirective {
constructor(el, renderer, ngZone) {
this.el = el;
this.renderer = renderer;
this.ngZone = ngZone;
/** 边框圆角半径 单位px */
this.ncBorderRadius = 24;
/** 边框线宽 单位px */
this.ncBorderWidth = 2;
/** 电弧宽度 单位px */
this.ncElectricWidth = 1;
/** 颜色 */
this.ncColor = '#5227FF';
/** 动画速度倍率 */
this.ncSpeed = 1;
/** 扰动强度(振幅) */
this.ncChaos = 0.12;
/** 停止动画时间,ms,0表示不停止 */
this.ncStopTime = 0;
/** 是否启用动画 */
this._enabled = true;
/** 双向绑定输出事件 */
this.ncEnabledChange = new EventEmitter();
// 动画状态
this.animationId = null;
this.time = 0;
this.lastFrameTime = 0;
this.isInitialized = false;
}
set ncEnabled(val) {
this._enabled = val !== null && val !== undefined && val !== false && val !== 'false';
}
get ncEnabled() {
return this._enabled;
}
ngOnInit() {
this.setupHost();
this.createDom();
this.injectStyles();
this.isInitialized = true;
if (this.ncEnabled) {
this.startAnimation();
}
}
ngOnChanges(changes) {
if (!this.isInitialized)
return;
// ncColor / ncSpeed / ncChaos / ncElectricWidth / ncBorderRadius 在 drawFrame() 中每帧直接读取,变更自动生效,无需处理。
// ncColor 除 canvas strokeStyle 外还驱动三个 glow DOM 元素,需显式更新。
if (changes['ncColor']) {
this.applyColorVar();
}
// ncBorderRadius 除 canvas路径圆角 外还影响宿主元素 CSS border-radius(glow 层用 inherit 会跟随),需显式更新。
if (changes['ncBorderRadius']) {
this.applyBorderRadius();
}
// ncBorderWidth 影响 glow 层 border 宽度(需显式更新)
if (changes['ncBorderWidth']) {
this.applyBorderWidth();
}
// ncEnabled 变化时启停动画
if (changes['ncEnabled']) {
if (this.ncEnabled) {
this.startAnimation();
}
else {
this.stopAnimation();
}
}
}
ngOnDestroy() {
this.stopAnimation();
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.stopTimer) {
clearTimeout(this.stopTimer);
}
}
// ─── 初始化 ─────────────────────────────────────────────────────────────────
setupHost() {
const host = this.el.nativeElement;
const pos = getComputedStyle(host).position;
if (!pos || pos === 'static') {
this.renderer.setStyle(host, 'position', 'relative');
}
this.renderer.setStyle(host, 'isolation', 'isolate');
this.renderer.setStyle(host, 'overflow', 'visible');
this.applyColorVar();
this.applyBorderRadius();
}
applyBorderRadius() {
this.renderer.setStyle(this.el.nativeElement, 'border-radius', `${this.ncBorderRadius}px`);
}
applyBorderWidth() {
if (this.glow1El) {
this.renderer.setStyle(this.glow1El, 'border-width', `${this.ncBorderWidth}px`);
}
if (this.glow2El) {
this.renderer.setStyle(this.glow2El, 'border-width', `${this.ncBorderWidth}px`);
}
}
applyColorVar() {
this.renderer.setStyle(this.el.nativeElement, '--electric-border-color', this.ncColor);
// 同步更新已存在的 glow 层颜色(ngOnChanges 时也能生效)
if (this.glow1El) {
this.renderer.setStyle(this.glow1El, 'border-color', `oklch(from ${this.ncColor} l c h / 0.6)`);
}
if (this.glow2El) {
this.renderer.setStyle(this.glow2El, 'border-color', this.ncColor);
}
if (this.bgGlowEl) {
this.renderer.setStyle(this.bgGlowEl, 'background', `linear-gradient(-30deg, ${this.ncColor}, transparent, ${this.ncColor})`);
}
}
createDom() {
const host = this.el.nativeElement;
// ── canvas 容器 ──
this.canvasContainerEl = this.renderer.createElement('div');
this.renderer.addClass(this.canvasContainerEl, 'nc-eb-canvas-container');
this.setStyles(this.canvasContainerEl, {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
'pointer-events': 'none',
'z-index': '2'
});
// ── canvas ──
this.canvasEl = this.renderer.createElement('canvas');
this.renderer.addClass(this.canvasEl, 'nc-eb-canvas');
this.renderer.setStyle(this.canvasEl, 'display', 'block');
this.renderer.appendChild(this.canvasContainerEl, this.canvasEl);
this.renderer.appendChild(host, this.canvasContainerEl);
// ── glow 层容器 ──
this.layersEl = this.renderer.createElement('div');
this.renderer.addClass(this.layersEl, 'nc-eb-layers');
this.setStyles(this.layersEl, {
position: 'absolute',
inset: '0',
'border-radius': 'inherit',
'pointer-events': 'none',
'z-index': '0'
});
const glowBase = {
position: 'absolute',
inset: '0',
'border-radius': 'inherit',
'pointer-events': 'none',
'box-sizing': 'border-box'
};
// nc-eb-glow-1
this.glow1El = this.renderer.createElement('div');
this.renderer.addClass(this.glow1El, 'nc-eb-glow-1');
this.setStyles(this.glow1El, Object.assign(Object.assign({}, glowBase), { border: `${this.ncBorderWidth}px solid ${this.ncColor}`, filter: 'blur(1px)' }));
// nc-eb-glow-2
this.glow2El = this.renderer.createElement('div');
this.renderer.addClass(this.glow2El, 'nc-eb-glow-2');
this.setStyles(this.glow2El, Object.assign(Object.assign({}, glowBase), { border: `${this.ncBorderWidth}px solid ${this.ncColor}`, filter: 'blur(4px)' }));
// nc-eb-background-glow
this.bgGlowEl = this.renderer.createElement('div');
this.renderer.addClass(this.bgGlowEl, 'nc-eb-background-glow');
this.setStyles(this.bgGlowEl, Object.assign(Object.assign({}, glowBase), { 'z-index': '-1', transform: 'scale(1.1)', filter: 'blur(32px)', opacity: '0.3', background: `linear-gradient(-30deg, ${this.ncColor}, transparent, ${this.ncColor})` }));
this.renderer.appendChild(this.layersEl, this.glow1El);
this.renderer.appendChild(this.layersEl, this.glow2El);
this.renderer.appendChild(this.layersEl, this.bgGlowEl);
this.renderer.appendChild(host, this.layersEl);
}
injectStyles() {
// oklch() CSS 函数需要现代浏览器,此处通过 <style> 注入 eb-glow 透明度变量
this.styleEl = this.renderer.createElement('style');
const css = `
[ncElectricBorder] {
--electric-light-color: oklch(from var(--electric-border-color) l c h);
}
`;
this.renderer.appendChild(this.styleEl, this.renderer.createText(css));
this.renderer.appendChild(document.head, this.styleEl);
}
// ─── 动画控制 ────────────────────────────────────────────────────────────────
startAnimation() {
if (!this.canvasEl)
return;
// 显示 canvas 容器和 glow 层
this.renderer.setStyle(this.canvasContainerEl, 'display', 'block');
this.renderer.setStyle(this.layersEl, 'display', 'block');
// 重置时间(避免首帧大幅跳变)
this.lastFrameTime = 0;
this.ngZone.runOutsideAngular(() => {
// 启动 ResizeObserver
this.resizeObserver = new ResizeObserver(() => this.updateCanvasSize());
this.resizeObserver.observe(this.el.nativeElement);
this.updateCanvasSize();
// 启动 rAF 循环
const loop = (currentTime) => {
this.drawFrame(currentTime);
this.animationId = requestAnimationFrame(loop);
};
this.animationId = requestAnimationFrame(loop);
});
// 如果设置了停止时间,则在指定时间后停止
if (this.ncStopTime > 0) {
this.stopTimer = setTimeout(() => {
this.stopAnimation();
this.ncEnabled = false;
this.ncEnabledChange.emit(false);
}, this.ncStopTime);
}
}
stopAnimation() {
if (this.animationId !== null) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.stopTimer) {
clearTimeout(this.stopTimer);
this.stopTimer = null;
}
// 隐藏 canvas 容器和 glow 层
if (this.canvasContainerEl) {
this.renderer.setStyle(this.canvasContainerEl, 'display', 'none');
}
if (this.layersEl) {
this.renderer.setStyle(this.layersEl, 'display', 'none');
}
}
// ─── Canvas 绘制 ─────────────────────────────────────────────────────────────
updateCanvasSize() {
const container = this.el.nativeElement;
const rect = container.getBoundingClientRect();
const borderOffset = 60;
const width = rect.width + borderOffset * 2;
const height = rect.height + borderOffset * 2;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
this.canvasEl.width = width * dpr;
this.canvasEl.height = height * dpr;
this.canvasEl.style.width = `${width}px`;
this.canvasEl.style.height = `${height}px`;
}
drawFrame(currentTime) {
const canvas = this.canvasEl;
const ctx = canvas.getContext('2d');
if (!ctx)
return;
const deltaTime = this.lastFrameTime === 0 ? 0 : (currentTime - this.lastFrameTime) / 1000;
this.time += deltaTime * this.ncSpeed;
this.lastFrameTime = currentTime;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.scale(dpr, dpr);
ctx.strokeStyle = this.ncColor;
ctx.lineWidth = this.ncElectricWidth;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
const borderOffset = 60;
const displacement = 60;
const canvasW = canvas.width / dpr;
const canvasH = canvas.height / dpr;
const left = borderOffset;
const top = borderOffset;
const borderWidth = canvasW - 2 * borderOffset;
const borderHeight = canvasH - 2 * borderOffset;
const maxRadius = Math.min(borderWidth, borderHeight) / 2;
const radius = Math.min(this.ncBorderRadius, maxRadius);
const approximatePerimeter = 2 * (borderWidth + borderHeight) + 2 * Math.PI * radius;
const sampleCount = Math.floor(approximatePerimeter / 2);
// 噪声参数
const octaves = 10;
const lacunarity = 1.6;
const gain = 0.7;
const amplitude = this.ncChaos;
const frequency = 10;
const baseFlatness = 0;
ctx.beginPath();
for (let i = 0; i <= sampleCount; i++) {
const progress = i / sampleCount;
const point = this.getRoundedRectPoint(progress, left, top, borderWidth, borderHeight, radius);
const xNoise = this.octavedNoise(progress * 8, octaves, lacunarity, gain, amplitude, frequency, this.time, 0, baseFlatness);
const yNoise = this.octavedNoise(progress * 8, octaves, lacunarity, gain, amplitude, frequency, this.time, 1, baseFlatness);
const displacedX = point.x + xNoise * displacement;
const displacedY = point.y + yNoise * displacement;
if (i === 0) {
ctx.moveTo(displacedX, displacedY);
}
else {
ctx.lineTo(displacedX, displacedY);
}
}
ctx.closePath();
ctx.stroke();
}
// ─── 几何与噪声工具方法 ───────────────────────────────────────────────────────
random(x) {
return (Math.sin(x * 12.9898) * 43758.5453) % 1;
}
noise2D(x, y) {
const i = Math.floor(x);
const j = Math.floor(y);
const fx = x - i;
const fy = y - j;
const a = this.random(i + j * 57);
const b = this.random(i + 1 + j * 57);
const c = this.random(i + (j + 1) * 57);
const d = this.random(i + 1 + (j + 1) * 57);
const ux = fx * fx * (3.0 - 2.0 * fx);
const uy = fy * fy * (3.0 - 2.0 * fy);
return a * (1 - ux) * (1 - uy) + b * ux * (1 - uy) + c * (1 - ux) * uy + d * ux * uy;
}
octavedNoise(x, octaves, lacunarity, gain, baseAmplitude, baseFrequency, time, seed, baseFlatness) {
let y = 0;
let amplitude = baseAmplitude;
let frequency = baseFrequency;
for (let i = 0; i < octaves; i++) {
let octaveAmplitude = amplitude;
if (i === 0)
octaveAmplitude *= baseFlatness;
y += octaveAmplitude * this.noise2D(frequency * x + seed * 100, time * frequency * 0.3);
frequency *= lacunarity;
amplitude *= gain;
}
return y;
}
getCornerPoint(centerX, centerY, radius, startAngle, arcLength, progress) {
const angle = startAngle + progress * arcLength;
return { x: centerX + radius * Math.cos(angle), y: centerY + radius * Math.sin(angle) };
}
getRoundedRectPoint(t, left, top, width, height, radius) {
const straightWidth = width - 2 * radius;
const straightHeight = height - 2 * radius;
const cornerArc = (Math.PI * radius) / 2;
const totalPerimeter = 2 * straightWidth + 2 * straightHeight + 4 * cornerArc;
const distance = t * totalPerimeter;
let accumulated = 0;
// Top edge
if (distance <= accumulated + straightWidth) {
const progress = (distance - accumulated) / straightWidth;
return { x: left + radius + progress * straightWidth, y: top };
}
accumulated += straightWidth;
// Top-right corner
if (distance <= accumulated + cornerArc) {
const progress = (distance - accumulated) / cornerArc;
return this.getCornerPoint(left + width - radius, top + radius, radius, -Math.PI / 2, Math.PI / 2, progress);
}
accumulated += cornerArc;
// Right edge
if (distance <= accumulated + straightHeight) {
const progress = (distance - accumulated) / straightHeight;
return { x: left + width, y: top + radius + progress * straightHeight };
}
accumulated += straightHeight;
// Bottom-right corner
if (distance <= accumulated + cornerArc) {
const progress = (distance - accumulated) / cornerArc;
return this.getCornerPoint(left + width - radius, top + height - radius, radius, 0, Math.PI / 2, progress);
}
accumulated += cornerArc;
// Bottom edge
if (distance <= accumulated + straightWidth) {
const progress = (distance - accumulated) / straightWidth;
return { x: left + width - radius - progress * straightWidth, y: top + height };
}
accumulated += straightWidth;
// Bottom-left corner
if (distance <= accumulated + cornerArc) {
const progress = (distance - accumulated) / cornerArc;
return this.getCornerPoint(left + radius, top + height - radius, radius, Math.PI / 2, Math.PI / 2, progress);
}
accumulated += cornerArc;
// Left edge
if (distance <= accumulated + straightHeight) {
const progress = (distance - accumulated) / straightHeight;
return { x: left, y: top + height - radius - progress * straightHeight };
}
accumulated += straightHeight;
// Top-left corner
const progress = (distance - accumulated) / cornerArc;
return this.getCornerPoint(left + radius, top + radius, radius, Math.PI, Math.PI / 2, progress);
}
// ─── 工具 ────────────────────────────────────────────────────────────────────
setStyles(el, styles) {
Object.entries(styles).forEach(([prop, value]) => {
this.renderer.setStyle(el, prop, value);
});
}
}
ElectricBorderDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.1.5", ngImport: i0, type: ElectricBorderDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive });
ElectricBorderDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.1.5", type: ElectricBorderDirective, selector: "[ncElectricBorder]", inputs: { ncBorderRadius: "ncBorderRadius", ncBorderWidth: "ncBorderWidth", ncElectricWidth: "ncElectricWidth", ncColor: "ncColor", ncSpeed: "ncSpeed", ncChaos: "ncChaos", ncStopTime: "ncStopTime", ncEnabled: "ncEnabled" }, outputs: { ncEnabledChange: "ncEnabledChange" }, usesOnChanges: true, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.1.5", ngImport: i0, type: ElectricBorderDirective, decorators: [{
type: Directive,
args: [{
selector: '[ncElectricBorder]'
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.NgZone }]; }, propDecorators: { ncBorderRadius: [{
type: Input
}], ncBorderWidth: [{
type: Input
}], ncElectricWidth: [{
type: Input
}], ncColor: [{
type: Input
}], ncSpeed: [{
type: Input
}], ncChaos: [{
type: Input
}], ncStopTime: [{
type: Input
}], ncEnabled: [{
type: Input
}], ncEnabledChange: [{
type: Output
}] } });
class NcElectricBorderModule {
}
NcElectricBorderModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.1.5", ngImport: i0, type: NcElectricBorderModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
NcElectricBorderModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "12.1.5", ngImport: i0, type: NcElectricBorderModule, declarations: [ElectricBorderDirective], exports: [ElectricBorderDirective] });
NcElectricBorderModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "12.1.5", ngImport: i0, type: NcElectricBorderModule, imports: [[]] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.1.5", ngImport: i0, type: NcElectricBorderModule, decorators: [{
type: NgModule,
args: [{
declarations: [
ElectricBorderDirective
],
imports: [],
exports: [
ElectricBorderDirective
]
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { ElectricBorderDirective, NcElectricBorderModule };
//# sourceMappingURL=ng-cw-v12-electric-border.js.map