Goker Logo Animation
1 min read
Göker Logo Animation
Our logo design is derived from the first two letters of "Göker": G and O. When positioned side-by-side, these two shapes visually resemble a face—more specifically, a pair of expressive eyes. This simple metaphor served as a foundation for building a minimalist yet interactive brand icon.
Visual Construction
- The tail of the G was stylized to resemble a nose.
- The O sits beside it to complete a pair of eyes.
- Pupils are embedded as SVG
<circle>
elements within the eye shapes.
Interactive Layers
We enhanced the visual identity with micro-interactions:
Mouse Tracking Pupils
- Pupils follow the user's cursor.
- Distance affects scale, proximity makes them larger.
- Controlled using helper functions like
calculateDistance
,calculateScale
, andgetRotation
.
Blink Animation
- Triggered on
hover
, the eyes blink once or twice. - Done using a
mask
shaped exactly like the eyes, animated viatransform
onSVGPathElement
.
- Triggered on
Playful Recoil
- On hover, the entire face slightly pulls back and rotates, like a character reacting with attention.
Source Code
logo.utils.ts
type MousePosition = { x: number; y: number }type UpdateFunction = (pos: MousePosition) => voidlet mousePosition: MousePosition = { x: 0, y: 0 }const registry = new Set<UpdateFunction>()let frameRequested = falselet listenerAttached = falseconst updateAllComponents = () => {registry.forEach((updateCallback) => {try {updateCallback(mousePosition)} catch (error) {console.error('Error updating logo component:', error)}})frameRequested = false}const handleGlobalMouseMove = (event: MouseEvent) => {mousePosition = { x: event.clientX, y: event.clientY }if (!frameRequested) {frameRequested = truerequestAnimationFrame(updateAllComponents)}}export const registerLogoComponent = (updateCallback: UpdateFunction) => {registry.add(updateCallback)if (!listenerAttached && registry.size > 0) {document.addEventListener('mousemove', handleGlobalMouseMove)listenerAttached = true}}export const unregisterLogoComponent = (updateCallback: UpdateFunction) => {registry.delete(updateCallback)if (listenerAttached && registry.size === 0) {document.removeEventListener('mousemove', handleGlobalMouseMove)listenerAttached = falseframeRequested = false}}export const calculateDistance = (x1: number, y1: number, x2: number, y2: number): number => {return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2))}export const calculateScale = (distance: number,maxDistance: number = 1600,minScale: number = 0.75,maxScale: number = 1.5): number => {const normalizedDistance = Math.min(distance / maxDistance, 1)const invertedDistance = 1 - normalizedDistancereturn minScale + invertedDistance * (maxScale - minScale)}export const getRotation = (x: number, y: number, centerX: number, centerY: number): number => {return Math.atan2(y - centerY, x - centerX) * (180 / Math.PI)}
LogoAnimation.tsx
'use client'import { useEffect, useRef, useCallback } from 'react'import {registerLogoComponent,unregisterLogoComponent,calculateDistance,calculateScale,getRotation,} from '@/lib/logo.utils'type Props = {className?: string}const ROTATION_OFFSET = 40const MAX_PUPIL_OFFSET = 20const LogoAnimation = ({ className = '' }: Props) => {const pupilL = useRef<SVGCircleElement>(null)const pupilR = useRef<SVGCircleElement>(null)const eyelid = useRef<SVGPathElement>(null)const applyTransform = useCallback((pupil: SVGCircleElement,targetX: number,targetY: number,distance: number,rotation: number,) => {const scale = Math.min(Math.max(calculateScale(distance), 0.75), 2)pupil.style.transformOrigin = 'center'pupil.style.transformBox = 'fill-box'const offset = Math.min(Math.hypot(targetX, targetY), MAX_PUPIL_OFFSET)const angle = Math.atan2(targetY, targetX)const tx = offset * Math.cos(angle)const ty = offset * Math.sin(angle)pupil.style.transform = `translate(${tx}px, ${ty}px) rotate(${rotation - ROTATION_OFFSET}deg) scale(${scale})`},[])const updatePupils = useCallback((mousePos: { x: number; y: number }) => {if (!pupilL.current || !pupilR.current) returnconst rectL = pupilL.current.getBoundingClientRect()const rectR = pupilR.current.getBoundingClientRect()const centerL = { x: rectL.left + rectL.width / 2, y: rectL.top + rectL.height / 2 }const centerR = { x: rectR.left + rectR.width / 2, y: rectR.top + rectR.height / 2 }const dL = calculateDistance(mousePos.x, mousePos.y, centerL.x, centerL.y)const dR = calculateDistance(mousePos.x, mousePos.y, centerR.x, centerR.y)const rL = getRotation(mousePos.x, mousePos.y, centerL.x, centerL.y)const rR = getRotation(mousePos.x, mousePos.y, centerR.x, centerR.y)applyTransform(pupilL.current, mousePos.x - centerL.x, mousePos.y - centerL.y, dL, rL)applyTransform(pupilR.current, mousePos.x - centerR.x, mousePos.y - centerR.y, dR, rR)},[applyTransform])const handleBlink = useCallback(() => {if (!eyelid.current) returnconst element = eyelid.currentelement.setAttribute('transform', 'scale(1, 0.1) translate(0, 450)')setTimeout(() => {element.setAttribute('transform', 'scale(1, 1) translate(0, 0)')}, 250)}, [])useEffect(() => {registerLogoComponent(updatePupils)return () => unregisterLogoComponent(updatePupils)}, [updatePupils])return (<svgclassName={`logo-animation group ${className}`}xmlns="http://www.w3.org/2000/svg"viewBox="0 0 278.08 278.08"onMouseEnter={handleBlink}><defs><mask id="eyelid-mask"><pathref={eyelid}fill="white"d="M73.3,152.7c-28,0-50.7-24.9-50.7-55.6s22.7-55.6,50.7-55.6,50.7,24.9,50.7,55.6-22.7,55.6-50.7,55.6ZM195.1,145.7c-27.8,0-50.3-23.9-50.3-53.3s22.5-53.3,50.3-53.3,50.3,23.9,50.3,53.3-22.5,53.3-50.3,53.3Z"/></mask></defs><g mask="url(#eyelid-mask)"><circle ref={pupilL} cx="74.3" cy="96.5" r="15.7" /><circle ref={pupilR} cx="195.1" cy="92.4" r="15.7" /></g></svg>)}export default LogoAnimation
This animation now lives in production — minimal, expressive, and interactive. Whether it's a prompt input companion or a standalone mascot, it's a subtle reminder that even logos can have personality.
Hoping to enrich your brand with simple yet effective micro-interactions? This structure, built on clean code and a strong metaphor, might provide some inspiration.