How EFQRCode Synchronizes Animated Icons and Watermarks: A Technical Deep Dive

EFQRCode synchronizes animated icons and watermarks by calculating the least common multiple (LCM) of their total durations, repeating frame sequences as necessary, and rendering synchronized QR code frames in parallel across all CPU cores.

When generating animated QR codes with overlay assets, timing mismatches between animated icons and watermarks can cause visual desynchronization. The EFQRCode library solves this through a frame reconciliation pipeline implemented in EFQRCode+Generator.swift. This article examines the exact algorithms used to align multi-layer animations and produce synchronized output.

The Challenge of Multi-Layer Animation Synchronization

Animated QR codes in EFQRCode support both icons (center images) and watermarks (background or overlay images). Each asset can be either static or animated with independent frame rates and durations. When both layers animate simultaneously, the generator must create a unified timeline where every QR code frame contains the correct paired states of both animations.

Without synchronization, a 10-frame icon animation paired with a 15-frame watermark would loop at different rates, causing the visual relationship between layers to drift continuously.

How EFQRCode Extracts Frame Data

Before synchronization can occur, the library normalizes both static and animated image sources into a consistent frame-based representation.

frameImages: Normalizing Static and Animated Sources

The frameImages(image:) method in EFQRCode+Generator.swift (lines 991-997) converts an EFStyleParamImage enum into a tuple of frame arrays and delay arrays:

private func frameImages(image: EFStyleParamImage) -> ([CGImage], [CGFloat]) {
    switch image {
    case .static(let image):
        return ([image], [1])
    case .animated(let images, let imageDelays):
        return (images, imageDelays)
    }
}

Static images return a single frame with a 1-second duration placeholder, while animated images preserve their original frame arrays and per-frame delays. This normalization allows the synchronization algorithm to treat both types uniformly as temporal sequences.

Synchronizing Animated Icons and Watermarks with LCM

Once frame data is extracted, EFQRCode calculates a common timeline that accommodates both animations without dropping frames or altering playback speed.

reconcileFrameImages: Computing the Common Timeline

The reconcileFrameImages(image1:image2:) method (lines 845-873 in EFQRCode+Generator.swift) computes the least common multiple (LCM) of the two animation durations. This determines the smallest time window in which both animations complete an integer number of loops.

The algorithm follows these steps:

  1. Calculate total durations for both sequences by summing their per-frame delays
  2. Compute LCM of the two durations using integer arithmetic (handling CGFloat values via rational approximation)
  3. Determine repeat counts by dividing the LCM by each animation's duration
  4. Build synchronized arrays by repeating each frame sequence the calculated number of times and interleaving delays

The resulting arrays contain identical frame counts, ensuring that when the QR code generator iterates through index i, it retrieves the correct corresponding frames from both the icon and watermark sequences.

Parallel Frame Generation Pipeline

With synchronized frame arrays established, EFQRCode renders the final QR code animation using parallel processing to maximize performance.

reconcileQRImages: Rendering Synchronized Frames

The reconcileQRImages(image1:image2:style:size:) method (lines 998-1020) orchestrates the final rendering pipeline:

let (iconFrames, watermarkFrames, delays) = self.reconcileFrameImages(...)
var qrFrames = [CGImage?](repeating: nil, count: delays.count)

for index in 0..<delays.count {
    queue.addOperation {
        let iconImage = iconFrames.isEmpty ? nil : .static(image: iconFrames[index])
        let watermarkImage = watermarkFrames.isEmpty ? nil : .static(image: watermarkFrames[index])
        let frameStyle = style.copyWith(iconImage: iconImage, watermarkImage: watermarkImage)
        let tempGenerator = EFQRCode.Generator(self.qrcode, styleImplementation: frameStyle)
        let qrFrame = try tempGenerator.toImage(size: size)
        qrFrames[index] = qrFrame.cgImage()
    }
}

For each synchronized frame index, the method:

  • Wraps the current icon and watermark frames in EFStyleParamImage.static containers
  • Creates a modified style via copyWith(iconImage:watermarkImage:) to apply these specific frames
  • Instantiates a temporary EFQRCode.Generator with the frame-specific style
  • Renders the QR code image asynchronously using an OperationQueue

This parallel approach utilizes all available CPU cores, significantly reducing generation time for high-frame-count animations.

Complete Implementation Example

The following Swift code demonstrates loading animated assets and generating a synchronized QR code:

import EFQRCode
import CoreGraphics

// 1. Load animated icon frames (e.g., from a GIF)
let iconFrames: [CGImage] = // ... array of CGImage
let iconDelays: [CGFloat] = // ... per-frame delays in seconds
let animatedIcon = EFStyleParamImage.animated(images: iconFrames,
                                              imageDelays: iconDelays)

// 2. Load animated watermark frames
let wmFrames: [CGImage] = // ... array of CGImage
let wmDelays: [CGFloat] = // ... per-frame delays
let animatedWatermark = EFStyleParamImage.animated(images: wmFrames,
                                                   imageDelays: wmDelays)

// 3. Build a style that uses both animated layers
let style = EFQRCodeStyle()
    .with(icon: animatedIcon)
    .with(watermark: animatedWatermark)

// 4. Generate an animated GIF
// The library automatically invokes reconcileQRImages to synchronize frames
let generator = EFQRCode.Generator("https://example.com", styleImplementation: style)
let gifData = try generator.createGIF(size: CGSize(width: 300, height: 300))

When createGIF executes, it internally triggers the synchronization pipeline described above, ensuring the icon and watermark animations remain perfectly aligned throughout the output GIF.

Summary

  • EFQRCode treats icons and watermarks as EFStyleParamImage enums that can be static or animated, normalizing them through frameImages(image:) in EFQRCode+Generator.swift.
  • Synchronization relies on calculating the least common multiple (LCM) of animation durations via reconcileFrameImages(image1:image2:), ensuring both sequences loop perfectly within the same timeframe.
  • Frame generation occurs in parallel using reconcileQRImages(image1:image2:style:size:), which creates temporary generator instances for each synchronized frame pair using copyWith(iconImage:watermarkImage:).
  • The pipeline supports any combination of static and animated assets, automatically handling timing reconciliation only when both parameters contain animation data.

Frequently Asked Questions

How does EFQRCode handle animations with different frame rates?

EFQRCode handles different frame rates by calculating the least common multiple (LCM) of the total durations of both animations. The reconcileFrameImages method in EFQRCode+Generator.swift determines how many times each sequence must repeat to achieve a common timeline, then interleaves the frames accordingly. This ensures that a 10-frame icon and a 15-frame watermark will loop in perfect synchronization without speed adjustments.

What is the LCM algorithm used for in EFQRCode?

The LCM (Least Common Multiple) algorithm in EFQRCode determines the smallest time window in which two animated sequences can complete an integer number of full loops. Located in the reconcileFrameImages function, it computes lcm(duration1, duration2) to establish a unified timeline. The algorithm then calculates repeat counts for each animation (duration / lcm) and builds synchronized frame arrays where both sequences align frame-by-frame.

Can EFQRCode synchronize more than two animated layers?

The current implementation in EFQRCode+Generator.swift specifically handles synchronization between two layers: the icon and the watermark. The reconcileFrameImages and reconcileQRImages methods accept exactly two image parameters. While the architecture could theoretically extend to support additional layers by chaining LCM calculations across multiple sequences, the existing codebase limits synchronization to the icon-watermark pair defined in the EFQRCodeStyle configuration.

Where is the synchronization logic located in the source code?

The synchronization logic resides primarily in Source/EFQRCode+Generator.swift. The key methods are frameImages(image:) at lines 991-997 for frame extraction, reconcileFrameImages(image1:image2:) at lines 845-873 for LCM-based timeline calculation, and reconcileQRImages(image1:image2:style:size:) at lines 998-1020 for parallel frame rendering. Supporting definitions for animated image parameters are located in Source/Styles/EFQRCodeStyle.swift.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →