This commit is contained in:
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
.background2-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Ensure smooth rendering */
|
||||
.background2-canvas {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
/* Dark mode specific adjustments */
|
||||
[data-theme='dark'] .background2-canvas {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Light mode specific adjustments */
|
||||
[data-theme='light'] .background2-canvas {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Animation performance optimization */
|
||||
.background2-canvas {
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
+604
@@ -0,0 +1,604 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
||||
|
||||
import { useDark } from '@rspress/core/runtime';
|
||||
import './index.css';
|
||||
|
||||
// Performance configuration based on device capabilities
|
||||
interface PerformanceConfig {
|
||||
enabled: boolean;
|
||||
meteorCount: number;
|
||||
maxFlameTrails: number;
|
||||
trailLength: number;
|
||||
animationQuality: 'high' | 'medium' | 'low';
|
||||
frameSkip: number;
|
||||
}
|
||||
|
||||
// Performance detection utilities
|
||||
const detectPerformance = (): PerformanceConfig => {
|
||||
// Check for reduced motion preference
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
return {
|
||||
enabled: false,
|
||||
meteorCount: 0,
|
||||
maxFlameTrails: 0,
|
||||
trailLength: 0,
|
||||
animationQuality: 'low',
|
||||
frameSkip: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Basic device capability detection
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return {
|
||||
enabled: false,
|
||||
meteorCount: 0,
|
||||
maxFlameTrails: 0,
|
||||
trailLength: 0,
|
||||
animationQuality: 'low',
|
||||
frameSkip: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Check hardware concurrency (CPU cores)
|
||||
const cores = navigator.hardwareConcurrency || 2;
|
||||
|
||||
// Check memory (if available)
|
||||
const memory = (navigator as any).deviceMemory || 4;
|
||||
|
||||
// Check if mobile device
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
);
|
||||
|
||||
// Performance scoring
|
||||
let score = 0;
|
||||
score += cores >= 4 ? 2 : cores >= 2 ? 1 : 0;
|
||||
score += memory >= 8 ? 2 : memory >= 4 ? 1 : 0;
|
||||
score += isMobile ? -1 : 1;
|
||||
|
||||
// Configure based on performance score
|
||||
if (score >= 4) {
|
||||
// High performance
|
||||
return {
|
||||
enabled: true,
|
||||
meteorCount: 12,
|
||||
maxFlameTrails: 15,
|
||||
trailLength: 20,
|
||||
animationQuality: 'high',
|
||||
frameSkip: 1,
|
||||
};
|
||||
} else if (score >= 2) {
|
||||
// Medium performance
|
||||
return {
|
||||
enabled: true,
|
||||
meteorCount: 8,
|
||||
maxFlameTrails: 8,
|
||||
trailLength: 12,
|
||||
animationQuality: 'medium',
|
||||
frameSkip: 2,
|
||||
};
|
||||
} else {
|
||||
// Low performance - disable
|
||||
return {
|
||||
enabled: false,
|
||||
meteorCount: 0,
|
||||
maxFlameTrails: 0,
|
||||
trailLength: 0,
|
||||
animationQuality: 'low',
|
||||
frameSkip: 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Trail particle interface for flame effect
|
||||
interface TrailParticle {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
size: number;
|
||||
tick: number;
|
||||
life: number;
|
||||
alpha: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
// Meteor particle interface definition
|
||||
interface MeteorParticle {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
radius: number;
|
||||
color: string;
|
||||
alpha: number;
|
||||
trail: Array<{ x: number; y: number; alpha: number }>;
|
||||
trailLength: number;
|
||||
speed: number;
|
||||
angle: number;
|
||||
// New flame trail system
|
||||
flameTrails: TrailParticle[];
|
||||
maxFlameTrails: number;
|
||||
}
|
||||
|
||||
// Configuration options for flame effects
|
||||
const flameOptions = {
|
||||
trailSizeBaseMultiplier: 0.6,
|
||||
trailSizeAddedMultiplier: 0.3,
|
||||
trailSizeSpeedMultiplier: 0.15,
|
||||
trailAddedBaseRadiant: -0.8,
|
||||
trailAddedAddedRadiant: 3,
|
||||
trailBaseLifeSpan: 25,
|
||||
trailAddedLifeSpan: 20,
|
||||
trailGenerationChance: 0.3,
|
||||
};
|
||||
|
||||
// Trail class for managing individual flame particles
|
||||
class Trail {
|
||||
private particle: TrailParticle;
|
||||
|
||||
private parentMeteor: MeteorParticle;
|
||||
|
||||
constructor(parent: MeteorParticle) {
|
||||
this.parentMeteor = parent;
|
||||
this.particle = this.createTrailParticle();
|
||||
}
|
||||
|
||||
// Create a new trail particle based on parent meteor
|
||||
private createTrailParticle = (): TrailParticle => {
|
||||
const baseSize =
|
||||
this.parentMeteor.radius *
|
||||
(flameOptions.trailSizeBaseMultiplier +
|
||||
flameOptions.trailSizeAddedMultiplier * Math.random());
|
||||
|
||||
const radiantOffset =
|
||||
flameOptions.trailAddedBaseRadiant + flameOptions.trailAddedAddedRadiant * Math.random();
|
||||
const trailAngle = this.parentMeteor.angle + radiantOffset;
|
||||
const speed = baseSize * flameOptions.trailSizeSpeedMultiplier;
|
||||
|
||||
return {
|
||||
x: this.parentMeteor.x + (Math.random() - 0.5) * this.parentMeteor.radius,
|
||||
y: this.parentMeteor.y + (Math.random() - 0.5) * this.parentMeteor.radius,
|
||||
vx: speed * Math.cos(trailAngle),
|
||||
vy: speed * Math.sin(trailAngle),
|
||||
size: baseSize,
|
||||
tick: 0,
|
||||
life: Math.floor(
|
||||
flameOptions.trailBaseLifeSpan + flameOptions.trailAddedLifeSpan * Math.random()
|
||||
),
|
||||
alpha: 0.8 + Math.random() * 0.2,
|
||||
color: this.parentMeteor.color,
|
||||
};
|
||||
};
|
||||
|
||||
// Update trail particle position and lifecycle
|
||||
public step = (): boolean => {
|
||||
this.particle.tick++;
|
||||
|
||||
// Check if trail particle should be removed
|
||||
if (this.particle.tick > this.particle.life) {
|
||||
return false; // Signal for removal
|
||||
}
|
||||
|
||||
// Update position
|
||||
this.particle.x += this.particle.vx;
|
||||
this.particle.y += this.particle.vy;
|
||||
|
||||
// Apply slight deceleration for more realistic flame behavior
|
||||
this.particle.vx *= 0.98;
|
||||
this.particle.vy *= 0.98;
|
||||
|
||||
return true; // Continue existing
|
||||
};
|
||||
|
||||
// Render the trail particle
|
||||
public draw = (ctx: CanvasRenderingContext2D): void => {
|
||||
const lifeRatio = 1 - this.particle.tick / this.particle.life;
|
||||
const currentSize = this.particle.size * lifeRatio;
|
||||
const currentAlpha = this.particle.alpha * lifeRatio;
|
||||
|
||||
if (currentSize <= 0 || currentAlpha <= 0) return;
|
||||
|
||||
const alphaHex = Math.floor(currentAlpha * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
|
||||
// Draw flame particle with gradient
|
||||
const gradient = ctx.createRadialGradient(
|
||||
this.particle.x,
|
||||
this.particle.y,
|
||||
0,
|
||||
this.particle.x,
|
||||
this.particle.y,
|
||||
currentSize * 2
|
||||
);
|
||||
|
||||
gradient.addColorStop(0, this.particle.color + 'FF');
|
||||
gradient.addColorStop(0.4, this.particle.color + alphaHex);
|
||||
gradient.addColorStop(0.8, this.particle.color + '33');
|
||||
gradient.addColorStop(1, this.particle.color + '00');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.particle.x, this.particle.y, currentSize, 0, Math.PI * 2);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
|
||||
// Add glow effect
|
||||
ctx.shadowColor = this.particle.color;
|
||||
ctx.shadowBlur = currentSize * 2;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
};
|
||||
}
|
||||
|
||||
// Background2 component - Circular meteor with enhanced flame trailing effect
|
||||
export const Background: React.FC = () => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const animationRef = useRef<number>(0);
|
||||
const meteorsRef = useRef<MeteorParticle[]>([]);
|
||||
const mouseRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const frameCountRef = useRef<number>(0);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const isDark = useDark();
|
||||
|
||||
// Performance configuration - memoized to avoid recalculation
|
||||
const performanceConfig = useMemo(() => detectPerformance(), []);
|
||||
|
||||
// Early return if animation is disabled
|
||||
if (!performanceConfig.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Color configuration - adjusted based on theme mode
|
||||
const lightColors = ['#4062A7', '#5482BE', '#5ABAC2', '#86C8C5'];
|
||||
const darkColors = ['#6B8CFF', '#8DA9FF', '#7FDBDA', '#A8EDEA'];
|
||||
const colors = isDark ? darkColors : lightColors;
|
||||
|
||||
// Initialize meteor particles with flame trail system - optimized based on performance
|
||||
const initMeteors = useCallback((): void => {
|
||||
meteorsRef.current = [];
|
||||
|
||||
for (let i = 0; i < performanceConfig.meteorCount; i++) {
|
||||
const angle = Math.PI * 0.25; // Fixed 45 degrees (down-right)
|
||||
const radius = Math.random() * 1.2 + 1.8; // Slightly larger meteors for better flame effect
|
||||
const speed = (radius - 1.8) * 3.0 + 2.0; // Adjusted speed range
|
||||
const trailLength = Math.floor(
|
||||
Math.random() * (performanceConfig.trailLength * 0.5) + performanceConfig.trailLength * 0.5
|
||||
);
|
||||
|
||||
meteorsRef.current.push({
|
||||
x: Math.random() * dimensions.width,
|
||||
y: Math.random() * dimensions.height,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
radius,
|
||||
color: colors[Math.floor(Math.random() * colors.length)],
|
||||
alpha: Math.random() * 0.3 + 0.7,
|
||||
trail: [],
|
||||
trailLength,
|
||||
speed,
|
||||
angle,
|
||||
// Initialize flame trail system with performance-based limits
|
||||
flameTrails: [],
|
||||
maxFlameTrails: Math.floor(
|
||||
radius * performanceConfig.maxFlameTrails * 0.5 + performanceConfig.maxFlameTrails * 0.3
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [performanceConfig, dimensions.width, dimensions.height, colors]);
|
||||
|
||||
// Update meteor trail (original trail system) - optimized
|
||||
const updateTrail = useCallback(
|
||||
(meteor: MeteorParticle): void => {
|
||||
meteor.trail.unshift({
|
||||
x: meteor.x,
|
||||
y: meteor.y,
|
||||
alpha: meteor.alpha,
|
||||
});
|
||||
|
||||
if (meteor.trail.length > meteor.trailLength) {
|
||||
meteor.trail.pop();
|
||||
}
|
||||
|
||||
// Only update alpha for visible trail points in high quality mode
|
||||
if (performanceConfig.animationQuality === 'high') {
|
||||
meteor.trail.forEach((point, index) => {
|
||||
point.alpha = meteor.alpha * (1 - index / meteor.trailLength);
|
||||
});
|
||||
}
|
||||
},
|
||||
[performanceConfig.animationQuality]
|
||||
);
|
||||
|
||||
// Update flame trails system - optimized
|
||||
const updateFlameTrails = useCallback(
|
||||
(meteor: MeteorParticle): void => {
|
||||
// Reduce flame trail generation based on performance config
|
||||
const generationChance =
|
||||
performanceConfig.animationQuality === 'high'
|
||||
? flameOptions.trailGenerationChance
|
||||
: performanceConfig.animationQuality === 'medium'
|
||||
? flameOptions.trailGenerationChance * 0.7
|
||||
: flameOptions.trailGenerationChance * 0.4;
|
||||
|
||||
// Generate new flame trail particles
|
||||
if (meteor.flameTrails.length < meteor.maxFlameTrails && Math.random() < generationChance) {
|
||||
const trail = new Trail(meteor);
|
||||
meteor.flameTrails.push(trail as any); // Type assertion for compatibility
|
||||
}
|
||||
|
||||
// Update existing flame trails and remove expired ones
|
||||
meteor.flameTrails = meteor.flameTrails.filter((trail: any) => trail.step && trail.step());
|
||||
},
|
||||
[performanceConfig.animationQuality]
|
||||
);
|
||||
|
||||
// Draw meteor with enhanced flame trailing effect - optimized
|
||||
const drawMeteor = useCallback(
|
||||
(ctx: CanvasRenderingContext2D, meteor: MeteorParticle): void => {
|
||||
// Draw flame trails first (behind the meteor) - skip in low quality mode
|
||||
if (performanceConfig.animationQuality !== 'low') {
|
||||
meteor.flameTrails.forEach((trail: any) => {
|
||||
if (trail.draw) {
|
||||
trail.draw(ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Draw original trail system with reduced complexity for lower quality
|
||||
const trailStep =
|
||||
performanceConfig.animationQuality === 'high'
|
||||
? 1
|
||||
: performanceConfig.animationQuality === 'medium'
|
||||
? 2
|
||||
: 3;
|
||||
|
||||
for (let i = 0; i < meteor.trail.length; i += trailStep) {
|
||||
const point = meteor.trail[i];
|
||||
const trailRadius = meteor.radius * (1 - i / meteor.trail.length) * 0.8;
|
||||
const alpha =
|
||||
performanceConfig.animationQuality === 'high'
|
||||
? point.alpha
|
||||
: meteor.alpha * (1 - i / meteor.trail.length);
|
||||
|
||||
const alphaHex = Math.floor(alpha * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(point.x, point.y, trailRadius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = meteor.color + alphaHex;
|
||||
ctx.fill();
|
||||
|
||||
// Reduce shadow effects for better performance
|
||||
if (performanceConfig.animationQuality === 'high') {
|
||||
ctx.shadowColor = meteor.color;
|
||||
ctx.shadowBlur = trailRadius * 2 + meteor.radius;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw main meteor body
|
||||
ctx.beginPath();
|
||||
ctx.arc(meteor.x, meteor.y, meteor.radius, 0, Math.PI * 2);
|
||||
|
||||
// Simplified gradient for lower quality modes
|
||||
if (performanceConfig.animationQuality === 'high') {
|
||||
const gradient = ctx.createRadialGradient(
|
||||
meteor.x,
|
||||
meteor.y,
|
||||
0,
|
||||
meteor.x,
|
||||
meteor.y,
|
||||
meteor.radius * 2.5
|
||||
);
|
||||
gradient.addColorStop(0, meteor.color + 'FF');
|
||||
gradient.addColorStop(0.5, meteor.color + 'DD');
|
||||
gradient.addColorStop(0.8, meteor.color + '77');
|
||||
gradient.addColorStop(1, meteor.color + '00');
|
||||
ctx.fillStyle = gradient;
|
||||
} else {
|
||||
ctx.fillStyle = meteor.color + 'DD';
|
||||
}
|
||||
|
||||
ctx.fill();
|
||||
|
||||
// Add bright core
|
||||
ctx.beginPath();
|
||||
ctx.arc(meteor.x, meteor.y, meteor.radius * 0.7, 0, Math.PI * 2);
|
||||
ctx.fillStyle = meteor.color + 'FF';
|
||||
ctx.fill();
|
||||
|
||||
// Enhanced outer glow - only in high quality mode
|
||||
if (performanceConfig.animationQuality === 'high') {
|
||||
ctx.shadowColor = meteor.color;
|
||||
ctx.shadowBlur = meteor.radius * 5 + 3;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
},
|
||||
[performanceConfig.animationQuality]
|
||||
);
|
||||
|
||||
// Update meteor position and behavior
|
||||
const updateMeteor = (meteor: MeteorParticle): void => {
|
||||
// Mouse interaction - meteors are slightly attracted to mouse
|
||||
const dx = mouseRef.current.x - meteor.x;
|
||||
const dy = mouseRef.current.y - meteor.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 120) {
|
||||
const force = ((120 - distance) / 120) * 0.008; // Slightly reduced force for stability
|
||||
const angle = Math.atan2(dy, dx);
|
||||
meteor.vx += Math.cos(angle) * force;
|
||||
meteor.vy += Math.sin(angle) * force;
|
||||
}
|
||||
|
||||
// Update both trail systems before moving
|
||||
updateTrail(meteor);
|
||||
updateFlameTrails(meteor);
|
||||
|
||||
// Update position
|
||||
meteor.x += meteor.vx;
|
||||
meteor.y += meteor.vy;
|
||||
|
||||
// Boundary wrapping with smooth transition
|
||||
if (meteor.x > dimensions.width + meteor.radius * 3) {
|
||||
meteor.x = -meteor.radius * 3;
|
||||
meteor.trail = []; // Clear trail when wrapping
|
||||
meteor.flameTrails = []; // Clear flame trails when wrapping
|
||||
}
|
||||
if (meteor.x < -meteor.radius * 3) {
|
||||
meteor.x = dimensions.width + meteor.radius * 3;
|
||||
meteor.trail = [];
|
||||
meteor.flameTrails = [];
|
||||
}
|
||||
if (meteor.y > dimensions.height + meteor.radius * 3) {
|
||||
meteor.y = -meteor.radius * 3;
|
||||
meteor.trail = [];
|
||||
meteor.flameTrails = [];
|
||||
}
|
||||
if (meteor.y < -meteor.radius * 3) {
|
||||
meteor.y = dimensions.height + meteor.radius * 3;
|
||||
meteor.trail = [];
|
||||
meteor.flameTrails = [];
|
||||
}
|
||||
|
||||
// Maintain consistent direction - gently guide back to base direction
|
||||
const baseAngle = Math.PI * 0.25; // 45 degrees
|
||||
|
||||
// Gradually adjust direction
|
||||
meteor.vx += Math.cos(baseAngle) * 0.003;
|
||||
meteor.vy += Math.sin(baseAngle) * 0.003;
|
||||
|
||||
// Apply slight damping to prevent excessive speed
|
||||
meteor.vx *= 0.999;
|
||||
meteor.vy *= 0.999;
|
||||
|
||||
// Maintain minimum speed to keep meteors moving
|
||||
const currentSpeed = Math.sqrt(meteor.vx * meteor.vx + meteor.vy * meteor.vy);
|
||||
if (currentSpeed < 0.5) {
|
||||
meteor.vx = Math.cos(baseAngle) * 0.8;
|
||||
meteor.vy = Math.sin(baseAngle) * 0.8;
|
||||
}
|
||||
|
||||
// Update meteor angle for flame trail generation
|
||||
meteor.angle = Math.atan2(meteor.vy, meteor.vx);
|
||||
};
|
||||
|
||||
// Animation loop - optimized with frame skipping
|
||||
const animate = useCallback((): void => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// Frame skipping for performance optimization
|
||||
frameCountRef.current++;
|
||||
if (frameCountRef.current % performanceConfig.frameSkip !== 0) {
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear canvas completely to avoid permanent trails
|
||||
ctx.clearRect(0, 0, dimensions.width, dimensions.height);
|
||||
|
||||
// Add subtle background with very low opacity for better flame visibility
|
||||
// Skip background overlay in low quality mode
|
||||
if (performanceConfig.animationQuality !== 'low') {
|
||||
const bgColor = isDark ? 'rgba(13, 17, 23, 0.015)' : 'rgba(237, 243, 248, 0.015)';
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.fillRect(0, 0, dimensions.width, dimensions.height);
|
||||
}
|
||||
|
||||
// Update and draw meteors
|
||||
meteorsRef.current.forEach((meteor) => {
|
||||
updateMeteor(meteor);
|
||||
drawMeteor(ctx, meteor);
|
||||
});
|
||||
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
}, [performanceConfig, dimensions, isDark, updateMeteor, drawMeteor]);
|
||||
|
||||
// Handle window resize - optimized
|
||||
useEffect(() => {
|
||||
const handleResize = (): void => {
|
||||
setDimensions({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// Handle mouse movement - optimized with throttling
|
||||
useEffect(() => {
|
||||
let throttleTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent): void => {
|
||||
if (throttleTimer) return;
|
||||
|
||||
throttleTimer = setTimeout(
|
||||
() => {
|
||||
mouseRef.current = { x: e.clientX, y: e.clientY };
|
||||
throttleTimer = null;
|
||||
},
|
||||
performanceConfig.animationQuality === 'high'
|
||||
? 16
|
||||
: performanceConfig.animationQuality === 'medium'
|
||||
? 32
|
||||
: 64
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
if (throttleTimer) {
|
||||
clearTimeout(throttleTimer);
|
||||
}
|
||||
};
|
||||
}, [performanceConfig.animationQuality]);
|
||||
|
||||
// Initialize and start animation - optimized
|
||||
useEffect(() => {
|
||||
if (dimensions.width === 0 || dimensions.height === 0) return;
|
||||
|
||||
initMeteors();
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
};
|
||||
}, [dimensions, isDark, initMeteors, animate]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
className="background2-canvas"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'none',
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
.flowgram-logo-container {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
width: calc(30vw - 64px);
|
||||
height: 550px;
|
||||
opacity: 0;
|
||||
|
||||
// Mobile responsive: move to top on small screens to prevent text overlap
|
||||
@media (max-width: 768px) {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
margin-bottom: 20px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Tablet responsive: adjust size and position
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
width: calc(45vw - 32px);
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
// Ensure minimum width to prevent squashing
|
||||
@media (min-width: 1025px) and (max-width: 1300px) {
|
||||
width: calc(45vw - 32px);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
// Ensure minimum width to prevent squashing
|
||||
@media (min-width: 1301) {
|
||||
width: calc(30vw - 64px);
|
||||
right: 15vw;
|
||||
}
|
||||
}
|
||||
|
||||
.flowgram-logo-mask {
|
||||
background-image: conic-gradient(from 180deg at 50% 50%, #4161a6, #5681bd, #4a9da3, #479590, #4161a6);
|
||||
filter: blur(120px);
|
||||
opacity: 0.4;
|
||||
z-index: 0;
|
||||
border-radius: 100%;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
animation: flowgram-logo-spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes flowgram-logo-spin {
|
||||
from {
|
||||
transform: translateX(20%) translateY(20%) rotate(0deg) scale(0.8);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(20%) translateY(20%) rotate(180deg) scale(1);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(20%) translateY(20%) rotate(360deg) scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.gedit-playground {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.gedit-playground-scroll-right-block {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gedit-playground-scroll-bottom-block {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.flowgram-logo-node {
|
||||
width: 60px;
|
||||
min-height: 150px;
|
||||
height: auto;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
cursor: move;
|
||||
transition: box-shadow 0.3s ease-in-out, transform 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
box-shadow:
|
||||
0 2px 6px 0 rgba(0, 0, 0, 0.04),
|
||||
0 4px 12px 0 rgba(0, 0, 0, 0.02),
|
||||
0 0 16px 2px rgba(var(--glow-color, 59, 130, 246), 0.6),
|
||||
0 0 32px 6px rgba(var(--glow-color, 59, 130, 246), 0.4),
|
||||
0 0 48px 12px rgba(var(--glow-color, 59, 130, 246), 0.25),
|
||||
0 0 64px 16px rgba(var(--glow-color, 59, 130, 246), 0.15);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import '@flowgram.ai/free-layout-editor/index.css';
|
||||
|
||||
import { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import './index.less';
|
||||
|
||||
import { useEditorProps } from './use-editor-props';
|
||||
import { FlowGramLogoMask } from './musk';
|
||||
|
||||
export const FlowGramLogo = () => {
|
||||
const editorProps = useEditorProps();
|
||||
return (
|
||||
<div className="flowgram-logo-container">
|
||||
<FlowGramLogoMask />
|
||||
<FreeLayoutEditorProvider {...editorProps}>
|
||||
<EditorRenderer />
|
||||
</FreeLayoutEditorProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
export const initialData: WorkflowJSON = {
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'start',
|
||||
meta: {
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'custom',
|
||||
meta: {
|
||||
position: { x: 110, y: 0 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'custom',
|
||||
meta: {
|
||||
position: { x: 220, y: 0 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'end',
|
||||
meta: {
|
||||
position: { x: 330, y: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
sourceNodeID: '1',
|
||||
targetNodeID: '2',
|
||||
},
|
||||
{
|
||||
sourceNodeID: '2',
|
||||
targetNodeID: '3',
|
||||
},
|
||||
{
|
||||
sourceNodeID: '3',
|
||||
targetNodeID: '4',
|
||||
},
|
||||
],
|
||||
};
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useDark } from '@rspress/core/runtime';
|
||||
|
||||
export const FlowGramLogoMask = () => {
|
||||
const isDark = useDark();
|
||||
return (
|
||||
<div
|
||||
className="flowgram-logo-mask"
|
||||
style={{
|
||||
backgroundImage: isDark
|
||||
? 'conic-gradient(from 180deg at 50% 50%, #0095ff 0deg, 180deg, #42d392 1turn)'
|
||||
: 'conic-gradient(from 180deg at 50% 50%, #3473fb 0deg, 180deg, #46cbc2 1turn)',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export const NodeColorMap: Record<string, string> = {
|
||||
'1': '#4161A6',
|
||||
'2': '#5681BD',
|
||||
'3': '#5DBBC1',
|
||||
'4': '#8BC9C5',
|
||||
};
|
||||
|
||||
export const NodeBorderColorMap: Record<string, string> = {
|
||||
'1': '#355397',
|
||||
'2': '#426ca7',
|
||||
'3': '#46a6ac',
|
||||
'4': '#7cbab6',
|
||||
};
|
||||
|
||||
export const NodeGlowColorMap: Record<string, string> = {
|
||||
'1': '141, 178, 254',
|
||||
'2': '145, 190, 254',
|
||||
'3': '155, 248, 254',
|
||||
'4': '191, 255, 250',
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
export const nodeRegistries: WorkflowNodeRegistry[] = [
|
||||
{
|
||||
type: 'start',
|
||||
meta: {
|
||||
isStart: true,
|
||||
deleteDisable: true,
|
||||
copyDisable: true,
|
||||
defaultPorts: [{ type: 'output' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'end',
|
||||
meta: {
|
||||
deleteDisable: true,
|
||||
copyDisable: true,
|
||||
defaultPorts: [{ type: 'input' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'custom',
|
||||
meta: {},
|
||||
defaultPorts: [{ type: 'output' }, { type: 'input' }],
|
||||
},
|
||||
];
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import '@flowgram.ai/free-layout-editor/index.css';
|
||||
|
||||
import {
|
||||
useNodeRender,
|
||||
WorkflowNodeProps,
|
||||
WorkflowNodeRenderer,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { PortRender } from './port';
|
||||
import { NodeBorderColorMap, NodeColorMap, NodeGlowColorMap } from './node-color';
|
||||
|
||||
export const NodeRender = (props: WorkflowNodeProps) => {
|
||||
const { selected, node, ports } = useNodeRender();
|
||||
const nodeColor = NodeColorMap[node.id] ?? '#fff';
|
||||
const borderColor = NodeBorderColorMap[node.id] ?? '#fff';
|
||||
const glowColor = NodeGlowColorMap[node.id] ?? '59, 130, 246';
|
||||
|
||||
return (
|
||||
<WorkflowNodeRenderer
|
||||
className="flowgram-logo-node"
|
||||
style={
|
||||
{
|
||||
background: nodeColor,
|
||||
border: selected ? `2px solid ${borderColor}` : `2px solid ${nodeColor}`,
|
||||
'--glow-color': glowColor,
|
||||
} as React.CSSProperties & { '--glow-color': string }
|
||||
}
|
||||
portStyle={{
|
||||
display: 'none',
|
||||
}}
|
||||
node={props.node}
|
||||
>
|
||||
{ports.map((p) => (
|
||||
<PortRender key={p.id} entity={p} />
|
||||
))}
|
||||
</WorkflowNodeRenderer>
|
||||
);
|
||||
};
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
useService,
|
||||
WorkflowHoverService,
|
||||
WorkflowLinesManager,
|
||||
WorkflowPortEntity,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { NodeColorMap } from './node-color';
|
||||
|
||||
export interface WorkflowPortRenderProps {
|
||||
entity: WorkflowPortEntity;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>, port: WorkflowPortEntity) => void;
|
||||
/** 激活状态颜色 (linked/hovered) */
|
||||
primaryColor?: string;
|
||||
/** 默认状态颜色 */
|
||||
secondaryColor?: string;
|
||||
/** 错误状态颜色 */
|
||||
errorColor?: string;
|
||||
/** 背景颜色 */
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
export const PortRender: React.FC<WorkflowPortRenderProps> =
|
||||
// eslint-disable-next-line react/display-name
|
||||
React.memo<WorkflowPortRenderProps>((props: WorkflowPortRenderProps) => {
|
||||
const hoverService = useService<WorkflowHoverService>(WorkflowHoverService);
|
||||
const { entity } = props;
|
||||
const { relativePosition } = entity;
|
||||
const [targetElement, setTargetElement] = useState(entity.targetElement);
|
||||
const [posX, updatePosX] = useState(relativePosition.x);
|
||||
const [posY, updatePosY] = useState(relativePosition.y);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// useEffect 时序问题可能导致 port.hasError 非最新,需重新触发一次 validate
|
||||
entity.validate();
|
||||
const dispose = entity.onEntityChange(() => {
|
||||
// 如果有挂载的节点,不需要更新位置信息
|
||||
if (entity.targetElement) {
|
||||
if (entity.targetElement !== targetElement) {
|
||||
setTargetElement(entity.targetElement);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const newPos = entity.relativePosition;
|
||||
// 加上 round 避免点位抖动
|
||||
updatePosX(Math.round(newPos.x));
|
||||
updatePosY(Math.round(newPos.y));
|
||||
});
|
||||
const dispose2 = hoverService.onHoveredChange((id) => {
|
||||
setHovered(hoverService.isHovered(entity.id));
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
dispose2.dispose();
|
||||
};
|
||||
}, [hoverService, entity, targetElement]);
|
||||
|
||||
// 构建 CSS 自定义属性用于颜色覆盖
|
||||
const colorStyles: Record<string, string> = {};
|
||||
if (props.primaryColor) {
|
||||
colorStyles['--g-workflow-port-color-primary'] = props.primaryColor;
|
||||
}
|
||||
if (props.secondaryColor) {
|
||||
colorStyles['--g-workflow-port-color-secondary'] = props.secondaryColor;
|
||||
}
|
||||
if (props.errorColor) {
|
||||
colorStyles['--g-workflow-port-color-error'] = props.errorColor;
|
||||
}
|
||||
if (props.backgroundColor) {
|
||||
colorStyles['--g-workflow-port-color-background'] = props.backgroundColor;
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
marginTop: '-12px',
|
||||
marginLeft: '-12px',
|
||||
position: 'absolute',
|
||||
background: '#fff',
|
||||
border: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
left: posX,
|
||||
top: posY,
|
||||
}}
|
||||
data-port-entity-id={entity.id}
|
||||
data-port-entity-type={entity.portType}
|
||||
data-testid="sdk.workflow.canvas.node.port"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: hovered ? '20px' : '16px',
|
||||
height: hovered ? '20px' : '16px',
|
||||
borderRadius: '50%',
|
||||
background: NodeColorMap[entity.node.id] ?? '#fff',
|
||||
transition: 'width 0.2s ease, height 0.2s ease',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return content;
|
||||
});
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import type { PositionSchema } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
export interface PositionGroup {
|
||||
[key: string]: PositionSchema;
|
||||
}
|
||||
|
||||
const originPosition: PositionGroup = {
|
||||
'1': {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
'2': {
|
||||
x: 110,
|
||||
y: 0,
|
||||
},
|
||||
'3': {
|
||||
x: 220,
|
||||
y: 0,
|
||||
},
|
||||
'4': {
|
||||
x: 330,
|
||||
y: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const positionGroups: PositionGroup[] = [
|
||||
// 水平线形态
|
||||
{
|
||||
'1': {
|
||||
x: 60,
|
||||
y: 0,
|
||||
},
|
||||
'2': {
|
||||
x: 120,
|
||||
y: 0,
|
||||
},
|
||||
'3': {
|
||||
x: 180,
|
||||
y: 0,
|
||||
},
|
||||
'4': {
|
||||
x: 240,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
originPosition,
|
||||
// 锯齿形态
|
||||
{
|
||||
'1': {
|
||||
x: 0,
|
||||
y: -40,
|
||||
},
|
||||
'2': {
|
||||
x: 110,
|
||||
y: 40,
|
||||
},
|
||||
'3': {
|
||||
x: 220,
|
||||
y: -40,
|
||||
},
|
||||
'4': {
|
||||
x: 330,
|
||||
y: 40,
|
||||
},
|
||||
},
|
||||
originPosition,
|
||||
// 弧形形态
|
||||
{
|
||||
'1': {
|
||||
x: 40,
|
||||
y: -30,
|
||||
},
|
||||
'2': {
|
||||
x: 120,
|
||||
y: -50,
|
||||
},
|
||||
'3': {
|
||||
x: 200,
|
||||
y: -50,
|
||||
},
|
||||
'4': {
|
||||
x: 280,
|
||||
y: -30,
|
||||
},
|
||||
},
|
||||
originPosition,
|
||||
// 散布形态
|
||||
{
|
||||
'1': {
|
||||
x: 30,
|
||||
y: 60,
|
||||
},
|
||||
'2': {
|
||||
x: 180,
|
||||
y: -40,
|
||||
},
|
||||
'3': {
|
||||
x: 80,
|
||||
y: -60,
|
||||
},
|
||||
'4': {
|
||||
x: 280,
|
||||
y: 40,
|
||||
},
|
||||
},
|
||||
originPosition,
|
||||
// 对角上升形态
|
||||
{
|
||||
'1': {
|
||||
x: 0,
|
||||
y: 75,
|
||||
},
|
||||
'2': {
|
||||
x: 110,
|
||||
y: 25,
|
||||
},
|
||||
'3': {
|
||||
x: 220,
|
||||
y: -25,
|
||||
},
|
||||
'4': {
|
||||
x: 330,
|
||||
y: -75,
|
||||
},
|
||||
},
|
||||
originPosition,
|
||||
// 波浪形态
|
||||
{
|
||||
'1': {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
'2': {
|
||||
x: 110,
|
||||
y: 80,
|
||||
},
|
||||
'3': {
|
||||
x: 220,
|
||||
y: 40,
|
||||
},
|
||||
'4': {
|
||||
x: 330,
|
||||
y: -20,
|
||||
},
|
||||
},
|
||||
originPosition,
|
||||
// 垂直堆叠形态
|
||||
{
|
||||
'1': {
|
||||
x: 165,
|
||||
y: -60,
|
||||
},
|
||||
'2': {
|
||||
x: 165,
|
||||
y: -20,
|
||||
},
|
||||
'3': {
|
||||
x: 165,
|
||||
y: 20,
|
||||
},
|
||||
'4': {
|
||||
x: 165,
|
||||
y: 60,
|
||||
},
|
||||
},
|
||||
originPosition,
|
||||
// 钻石形态
|
||||
{
|
||||
'1': {
|
||||
x: 165,
|
||||
y: -40,
|
||||
},
|
||||
'2': {
|
||||
x: 110,
|
||||
y: 0,
|
||||
},
|
||||
'3': {
|
||||
x: 165,
|
||||
y: 40,
|
||||
},
|
||||
'4': {
|
||||
x: 220,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
originPosition,
|
||||
];
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { delay, FreeLayoutPluginContext, startTween } from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { PositionGroup, positionGroups } from './position-groups';
|
||||
|
||||
const updateNodePosition = async (params: {
|
||||
ctx: FreeLayoutPluginContext;
|
||||
positionGroup: PositionGroup;
|
||||
}) => {
|
||||
const { ctx, positionGroup } = params;
|
||||
return new Promise<void>((resolve) => {
|
||||
startTween({
|
||||
from: { d: 0 },
|
||||
to: { d: 100 },
|
||||
duration: 1000,
|
||||
onUpdate: (v) => {
|
||||
ctx.document.getAllNodes().forEach((node) => {
|
||||
const { transform } = node.transform;
|
||||
const targetPosition = positionGroup[node.id] ?? {
|
||||
x: transform.position.x,
|
||||
y: transform.position.y,
|
||||
};
|
||||
transform.update({
|
||||
position: {
|
||||
x: transform.position.x + ((targetPosition.x - transform.position.x) * v.d) / 100,
|
||||
y: transform.position.y + ((targetPosition.y - transform.position.y) * v.d) / 100,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
onComplete: () => {
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const updatePosition = async (ctx: FreeLayoutPluginContext) => {
|
||||
// Cycle through position groups every 2 seconds
|
||||
let currentGroupIndex = 0;
|
||||
|
||||
// Use while loop instead of recursion to avoid stack overflow
|
||||
while (true) {
|
||||
// Wait for 2 seconds before next update
|
||||
await delay(2000);
|
||||
|
||||
// Update to current position group
|
||||
await updateNodePosition({
|
||||
ctx,
|
||||
positionGroup: positionGroups[currentGroupIndex],
|
||||
});
|
||||
|
||||
// Move to next position group (cycle back to 0 when reaching the end)
|
||||
currentGroupIndex = (currentGroupIndex + 1) % positionGroups.length;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
Field,
|
||||
FreeLayoutProps,
|
||||
PlaygroundConfigEntity,
|
||||
WorkflowLinesManager,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
|
||||
import { updatePosition } from './update-position';
|
||||
import { NodeRender } from './node-render';
|
||||
import { nodeRegistries } from './node-registries';
|
||||
import { NodeColorMap } from './node-color';
|
||||
import { initialData } from './initial-data';
|
||||
|
||||
export const useEditorProps = () =>
|
||||
useMemo<FreeLayoutProps>(
|
||||
() => ({
|
||||
background: false,
|
||||
materials: {
|
||||
renderDefaultNode: NodeRender,
|
||||
},
|
||||
isHideArrowLine: (ctx, line) => {
|
||||
if (line.from && line.to) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
nodeRegistries,
|
||||
initialData,
|
||||
onInit: (ctx) => {
|
||||
const linesManager = ctx.get(WorkflowLinesManager);
|
||||
linesManager.getLineColor = (line) => {
|
||||
const lineColor = NodeColorMap[line.from?.id ?? line.to?.id ?? ''] ?? '#000';
|
||||
return lineColor;
|
||||
};
|
||||
},
|
||||
onAllLayersRendered: async (ctx) => {
|
||||
await ctx.tools.fitView(false);
|
||||
// disable playground operations
|
||||
const playgroundConfig = ctx.get(PlaygroundConfigEntity);
|
||||
playgroundConfig.updateConfig = () => {};
|
||||
// display logo container
|
||||
const containerDOM = window.document.querySelector('.flowgram-logo-container');
|
||||
if (containerDOM instanceof HTMLDivElement) {
|
||||
containerDOM.style.opacity = '1';
|
||||
}
|
||||
// update nodes position
|
||||
updatePosition(ctx);
|
||||
},
|
||||
getNodeDefaultRegistry(type) {
|
||||
return {
|
||||
type,
|
||||
meta: {
|
||||
defaultExpanded: true,
|
||||
},
|
||||
formMeta: {
|
||||
/**
|
||||
* Render form
|
||||
*/
|
||||
render: () => (
|
||||
<>
|
||||
<Field<string> name="title">{({ field }) => <div>{field.value}</div>}</Field>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { LlmsContainer, LlmsCopyButton, LlmsViewOptions } from '@rspress/plugin-llms/runtime';
|
||||
import {
|
||||
HomeLayout as BaseHomeLayout,
|
||||
getCustomMDXComponent as basicGetCustomMDXComponent,
|
||||
} from '@rspress/core/theme-original';
|
||||
import { NoSSR, useDark } from '@rspress/core/runtime';
|
||||
|
||||
import { Background } from './components/background';
|
||||
|
||||
import './theme.css';
|
||||
import { FlowGramLogo } from './components/logo';
|
||||
import { useIsMobile } from './use-is-mobile';
|
||||
|
||||
function getCustomMDXComponent() {
|
||||
const { h1: H1, ...components } = basicGetCustomMDXComponent();
|
||||
|
||||
const MyH1 = ({ ...props }) => (
|
||||
<>
|
||||
<H1 {...props} />
|
||||
<LlmsContainer>
|
||||
<LlmsCopyButton />
|
||||
<LlmsViewOptions />
|
||||
</LlmsContainer>
|
||||
</>
|
||||
);
|
||||
return {
|
||||
...components,
|
||||
h1: MyH1,
|
||||
};
|
||||
}
|
||||
|
||||
function HomeLayout(props: Parameters<typeof BaseHomeLayout>[0]) {
|
||||
const isDark = useDark();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
<NoSSR>
|
||||
{isDark && !isMobile && <Background />}
|
||||
<FlowGramLogo />
|
||||
</NoSSR>
|
||||
<BaseHomeLayout {...props} afterHero={null} afterHeroActions={null} />
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { getCustomMDXComponent, HomeLayout };
|
||||
export * from '@rspress/core/theme-original';
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
:root {
|
||||
--rp-home-mask-background-image: transparent;
|
||||
}
|
||||
|
||||
.home-layout-container {
|
||||
position: relative;
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export const useIsMobile = (): boolean => {
|
||||
const [isMobile, setIsMobile] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkIsMobile = (): boolean =>
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
|
||||
setIsMobile(checkIsMobile());
|
||||
}, []);
|
||||
|
||||
return isMobile;
|
||||
};
|
||||
Reference in New Issue
Block a user