Make it stand out.

Whatever it is, the way you tell your story online can make all the difference.

import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react" import { addPropertyControls, ControlType, useIsStaticRenderer } from "framer" import { useInView } from "framer-motion" // User request: Create a new Framer code component named Global Journey Globe. It must render a self-contained interactive 3D-style globe suitable for a dark minimal portfolio section: a subtle rotating wireframe/landmass-like sphere on a transparent canvas, with animated glowing white curved flight arcs connecting Paris to Barcelona and Lisbon to London. Motion should be slow and ambient by default; users should be able to drag to rotate the globe. No labels, controls, pop-ups, icons, buttons, or tooltips. Keep it accessible with an aria label. Expose controls for width/height if feasible, motionSpeed defaulting slow, globeColor, arcColor, and background transparency. Avoid external runtime dependencies and remote data/API calls; ensure it renders reliably inside Framer and is responsive. interface MyComponentProps { width: number height: number motionSpeed: number globeColor: string arcColor: string backgroundTransparency: number ariaLabel: string style?: React.CSSProperties } type GeoPoint = { lat: number; lon: number } type Point3 = { x: number; y: number; z: number } type Point2 = { x: number; y: number; z: number; visible: boolean } const FALLBACK_SIZE = 420 const CITIES = { paris: { lat: 48.8566, lon: 2.3522 }, barcelona: { lat: 41.3874, lon: 2.1686 }, lisbon: { lat: 38.7223, lon: -9.1393 }, london: { lat: 51.5072, lon: -0.1276 }, } const CONTINENT_STROKES: GeoPoint[][] = [ [ { lat: 36, lon: -9 }, { lat: 44, lon: -5 }, { lat: 50, lon: 2 }, { lat: 54, lon: 8 }, { lat: 58, lon: 18 }, { lat: 55, lon: 27 }, { lat: 48, lon: 34 }, { lat: 42, lon: 27 }, { lat: 38, lon: 15 }, { lat: 36, lon: -2 }, { lat: 36, lon: -9 }, ], [ { lat: 15, lon: -18 }, { lat: 4, lon: -8 }, { lat: -5, lon: 10 }, { lat: -20, lon: 22 }, { lat: -34, lon: 19 }, { lat: -30, lon: 8 }, { lat: -13, lon: -4 }, { lat: 0, lon: -12 }, { lat: 15, lon: -18 }, ], ] /** * @framerSupportedLayoutWidth fixed * @framerSupportedLayoutHeight fixed */ export default function GlobalJourneyGlobe(props: MyComponentProps) { const { width, height, motionSpeed, globeColor, arcColor, backgroundTransparency, ariaLabel, style, } = props const isStatic = useIsStaticRenderer() const wrapperRef = useRef(null) const inView = useInView(wrapperRef, { amount: 0.2 }) const canvasRef = useRef(null) const requestRef = useRef(null) const yawRef = useRef(-0.35) const pitchRef = useRef(0.2) const dragRef = useRef(false) const lastPointerRef = useRef({ x: 0, y: 0 }) const sphereLines = useMemo(() => { const meridians: GeoPoint[][] = [] const parallels: GeoPoint[][] = [] for (let lon = -180; lon < 180; lon += 20) { const line: GeoPoint[] = [] for (let lat = -80; lat <= 80; lat += 4) line.push({ lat, lon }) meridians.push(line) } for (let lat = -70; lat <= 70; lat += 14) { const line: GeoPoint[] = [] for (let lon = -180; lon <= 180; lon += 4) line.push({ lat, lon }) parallels.push(line) } return [...meridians, ...parallels] }, []) const geoTo3D = useCallback((lat: number, lon: number, radius = 1): Point3 => { const latRad = (lat * Math.PI) / 180 const lonRad = (lon * Math.PI) / 180 return { x: radius * Math.cos(latRad) * Math.sin(lonRad), y: radius * Math.sin(latRad), z: radius * Math.cos(latRad) * Math.cos(lonRad), } }, []) const rotatePoint = useCallback((point: Point3, yaw: number, pitch: number): Point3 => { const cy = Math.cos(yaw) const sy = Math.sin(yaw) const cp = Math.cos(pitch) const sp = Math.sin(pitch) const x1 = point.x * cy - point.z * sy const z1 = point.x * sy + point.z * cy const y1 = point.y return { x: x1, y: y1 * cp - z1 * sp, z: y1 * sp + z1 * cp, } }, []) const project = useCallback( (point: Point3, radiusPx: number, centerX: number, centerY: number): Point2 => { const perspective = 2.6 const scale = perspective / (perspective - point.z) return { x: centerX + point.x * radiusPx * scale, y: centerY - point.y * radiusPx * scale, z: point.z, visible: point.z > -0.25, } }, [] ) const makeArcPoints = useCallback( (from: GeoPoint, to: GeoPoint, segments = 80): Point3[] => { const points: Point3[] = [] for (let i = 0; i <= segments; i++) { const t = i / segments const lat = from.lat + (to.lat - from.lat) * t const lon = from.lon + (to.lon - from.lon) * t const elevation = 1 + Math.sin(Math.PI * t) * 0.24 points.push(geoTo3D(lat, lon, elevation)) } return points }, [geoTo3D] ) const arcs = useMemo( () => [ makeArcPoints(CITIES.paris, CITIES.barcelona), makeArcPoints(CITIES.lisbon, CITIES.london), ], [makeArcPoints] ) const drawGlobe = useCallback( (timeMs: number, allowAnimation = true) => { const canvas = canvasRef.current if (!canvas) return const ctx = canvas.getContext("2d") if (!ctx) return const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1 const measuredWidth = wrapperRef.current?.clientWidth ?? 0 const measuredHeight = wrapperRef.current?.clientHeight ?? 0 const w = Math.max(1, measuredWidth || width || FALLBACK_SIZE) const h = Math.max(1, measuredHeight || height || FALLBACK_SIZE) if (canvas.width !== Math.floor(w * dpr) || canvas.height !== Math.floor(h * dpr)) { canvas.width = Math.floor(w * dpr) canvas.height = Math.floor(h * dpr) canvas.style.width = `${w}px` canvas.style.height = `${h}px` } ctx.setTransform(dpr, 0, 0, dpr, 0, 0) const alpha = Math.max(0, Math.min(1, 1 - backgroundTransparency)) ctx.clearRect(0, 0, w, h) if (alpha > 0) { ctx.fillStyle = `rgba(0, 0, 0, ${alpha})` ctx.fillRect(0, 0, w, h) } const t = timeMs * 0.001 if (!dragRef.current && !isStatic && allowAnimation) { yawRef.current += 0.0016 * motionSpeed pitchRef.current = 0.2 + Math.sin(t * 0.2) * 0.05 } const radius = Math.min(w, h) * 0.34 const cx = w / 2 const cy = h / 2 ctx.beginPath() ctx.arc(cx, cy, radius, 0, Math.PI * 2) ctx.strokeStyle = globeColor ctx.globalAlpha = 0.2 ctx.lineWidth = 1 ctx.stroke() ctx.globalAlpha = 1 sphereLines.forEach((line) => { let started = false ctx.beginPath() line.forEach((geo) => { const p = project( rotatePoint(geoTo3D(geo.lat, geo.lon), yawRef.current, pitchRef.current), radius, cx, cy ) if (p.visible) { if (!started) { ctx.moveTo(p.x, p.y) started = true } else { ctx.lineTo(p.x, p.y) } } else if (started) { started = false } }) ctx.strokeStyle = globeColor ctx.globalAlpha = 0.11 ctx.lineWidth = 1 ctx.stroke() }) CONTINENT_STROKES.forEach((shape) => { ctx.beginPath() shape.forEach((geo, idx) => { const p = project( rotatePoint(geoTo3D(geo.lat, geo.lon, 0.99), yawRef.current, pitchRef.current), radius, cx, cy ) if (idx === 0) ctx.moveTo(p.x, p.y) else ctx.lineTo(p.x, p.y) }) ctx.closePath() ctx.fillStyle = globeColor ctx.globalAlpha = 0.06 ctx.fill() ctx.strokeStyle = globeColor ctx.globalAlpha = 0.2 ctx.lineWidth = 1 ctx.stroke() }) const pulse = 0.5 + 0.5 * Math.sin(t * 1.2) arcs.forEach((arc, arcIndex) => { const reveal = (t * 0.07 * motionSpeed + arcIndex * 0.25) % 1 const revealCount = Math.max(8, Math.floor(arc.length * reveal)) ctx.beginPath() for (let i = 0; i < revealCount; i++) { const p = project( rotatePoint(arc[i], yawRef.current, pitchRef.current), radius, cx, cy ) if (!p.visible && i !== 0) continue if (i === 0) ctx.moveTo(p.x, p.y) else ctx.lineTo(p.x, p.y) } ctx.strokeStyle = arcColor ctx.lineWidth = 5 ctx.globalAlpha = 0.1 + pulse * 0.08 ctx.stroke() ctx.strokeStyle = arcColor ctx.lineWidth = 1.8 ctx.globalAlpha = 0.82 ctx.stroke() }) ctx.globalAlpha = 1 }, [ arcColor, arcs, backgroundTransparency, geoTo3D, globeColor, height, isStatic, motionSpeed, project, rotatePoint, sphereLines, width, ] ) useEffect(() => { if (typeof window === "undefined") return drawGlobe(0, false) }, [drawGlobe]) useLayoutEffect(() => { if (typeof window === "undefined") return let tries = 0 let rafId = 0 const drawWhenReady = () => { drawGlobe(0, false) const measuredWidth = wrapperRef.current?.clientWidth ?? 0 const measuredHeight = wrapperRef.current?.clientHeight ?? 0 tries += 1 if ((measuredWidth < 2 || measuredHeight < 2) && tries < 12) { rafId = window.requestAnimationFrame(drawWhenReady) } } rafId = window.requestAnimationFrame(drawWhenReady) let resizeObserver: ResizeObserver | null = null if (typeof ResizeObserver !== "undefined" && wrapperRef.current) { resizeObserver = new ResizeObserver(() => { drawGlobe(0, false) }) resizeObserver.observe(wrapperRef.current) } return () => { window.cancelAnimationFrame(rafId) resizeObserver?.disconnect() } }, [drawGlobe]) useEffect(() => { if (typeof window === "undefined") return if (isStatic || !inView) return const loop = (time: number) => { drawGlobe(time) requestRef.current = window.requestAnimationFrame(loop) } requestRef.current = window.requestAnimationFrame(loop) return () => { if (requestRef.current !== null) { window.cancelAnimationFrame(requestRef.current) } } }, [drawGlobe, inView, isStatic]) const onPointerDown = useCallback((event: React.PointerEvent) => { dragRef.current = true lastPointerRef.current = { x: event.clientX, y: event.clientY } }, []) const onPointerMove = useCallback( (event: React.PointerEvent) => { if (!dragRef.current) return const dx = event.clientX - lastPointerRef.current.x const dy = event.clientY - lastPointerRef.current.y lastPointerRef.current = { x: event.clientX, y: event.clientY } yawRef.current += dx * 0.006 pitchRef.current = Math.max(-1.1, Math.min(1.1, pitchRef.current + dy * 0.004)) drawGlobe(performance.now()) }, [drawGlobe] ) const onPointerUp = useCallback(() => { dragRef.current = false }, []) const resolvedWidth = width > 0 ? width : FALLBACK_SIZE const resolvedHeight = height > 0 ? height : FALLBACK_SIZE return (
) } addPropertyControls(GlobalJourneyGlobe, { width: { type: ControlType.Number, title: "Width", defaultValue: 420, min: 240, max: 1000, step: 1, unit: "px", }, height: { type: ControlType.Number, title: "Height", defaultValue: 420, min: 240, max: 1000, step: 1, unit: "px", }, motionSpeed: { type: ControlType.Number, title: "Motion", defaultValue: 0.35, min: 0.1, max: 2, step: 0.05, }, globeColor: { type: ControlType.Color, title: "Globe", defaultValue: "#FFFFFF", }, arcColor: { type: ControlType.Color, title: "Arcs", defaultValue: "#FFFFFF", }, backgroundTransparency: { type: ControlType.Number, title: "Bg Alpha", defaultValue: 1, min: 0, max: 1, step: 0.01, }, ariaLabel: { type: ControlType.String, title: "Aria", defaultValue: "Interactive rotating globe with flight paths", }, })

 

Make it stand out.

It all begins with an idea. Maybe you want to launch a business. Maybe you want to turn a hobby into something more. Or maybe you have a creative project to share with the world. Whatever it is, the way you tell your story online can make all the difference.

Make it stand out.

It all begins with an idea. Maybe you want to launch a business. Maybe you want to turn a hobby into something more. Or maybe you have a creative project to share with the world. Whatever it is, the way you tell your story online can make all the difference.

Make it stand out.

It all begins with an idea. Maybe you want to launch a business. Maybe you want to turn a hobby into something more. Or maybe you have a creative project to share with the world. Whatever it is, the way you tell your story online can make all the difference.

“It all begins with an idea. Maybe you want to launch a business. Maybe you want to turn a hobby into something more. Or maybe you have a creative project to share with the world. Whatever it is, the way you tell your story online can make all the difference.”

— Squarespace