///|
/// Structural analysis for choosing palettes, delta encoding, and previews.
pub(all) struct CanvasComplexity {
pixels : Int
opaque_pixels : Int
unique_colors : Int
edge_pixels : Int
entropy_milli : Int
luma_range : Int
} derive(Debug)
///|
pub fn CanvasComplexity::pixels(self : CanvasComplexity) -> Int {
self.pixels
}
///|
pub fn CanvasComplexity::opaque_pixels(self : CanvasComplexity) -> Int {
self.opaque_pixels
}
///|
pub fn CanvasComplexity::unique_colors(self : CanvasComplexity) -> Int {
self.unique_colors
}
///|
pub fn CanvasComplexity::edge_pixels(self : CanvasComplexity) -> Int {
self.edge_pixels
}
///|
pub fn CanvasComplexity::entropy_milli(self : CanvasComplexity) -> Int {
self.entropy_milli
}
///|
pub fn CanvasComplexity::luma_range(self : CanvasComplexity) -> Int {
self.luma_range
}
///|
fn entropy_component(count : Int, total : Int) -> Int {
if count <= 0 || total <= 0 {
0
} else {
let ratio = count * 1000 / total
let complement = 1000 - ratio
ratio * complement / 1000
}
}
///|
/// Estimate visual complexity using exact colors, edges, and luma spread.
pub fn Canvas::complexity(self : Canvas) -> CanvasComplexity {
let colors : Map[Int, Bool] = Map([])
let histogram = Array::make(256, 0)
let mut visible_count = 0
let mut edges = 0
let mut minimum = 255
let mut maximum = 0
for y in 0.. 0 {
visible_count += 1
}
if luma < minimum {
minimum = luma
}
if luma > maximum {
maximum = luma
}
if x > 0 {
let left = self.pixels[x - 1 + y * self.width]
if color.distance(left) > 24 {
edges += 1
}
}
if y > 0 {
let above = self.pixels[x + (y - 1) * self.width]
if color.distance(above) > 24 {
edges += 1
}
}
}
}
let total = self.pixels.length()
let mut entropy = 0
for count in histogram {
entropy += entropy_component(count, total)
}
CanvasComplexity::{
pixels: total,
opaque_pixels: visible_count,
unique_colors: colors.length(),
edge_pixels: edges,
entropy_milli: entropy,
luma_range: if total == 0 {
0
} else {
maximum - minimum
},
}
}
///|
pub(all) struct AnimationComplexity {
frame_count : Int
total_pixels : Int
changed_pixels : Int
mean_unique_colors : Int
active_frames : Int
peak_entropy : Int
} derive(Debug)
///|
pub fn AnimationComplexity::frame_count(self : AnimationComplexity) -> Int {
self.frame_count
}
///|
pub fn AnimationComplexity::total_pixels(self : AnimationComplexity) -> Int {
self.total_pixels
}
///|
pub fn AnimationComplexity::changed_pixels(self : AnimationComplexity) -> Int {
self.changed_pixels
}
///|
pub fn AnimationComplexity::mean_unique_colors(
self : AnimationComplexity,
) -> Int {
self.mean_unique_colors
}
///|
pub fn AnimationComplexity::active_frames(self : AnimationComplexity) -> Int {
self.active_frames
}
///|
pub fn AnimationComplexity::peak_entropy(self : AnimationComplexity) -> Int {
self.peak_entropy
}
///|
/// Analyze frame-to-frame activity and palette diversity.
pub fn Animation::complexity(self : Animation) -> AnimationComplexity {
let mut total_pixels = 0
let mut unique_total = 0
let mut active = 0
let mut peak_entropy = 0
let mut changed = 0
let mut previous = Canvas::new(
width=self.width,
height=self.height,
fill=self.background,
)
for frame in self.frames {
let current = frame.canvas()
let summary = current.complexity()
total_pixels += summary.pixels()
unique_total += summary.unique_colors()
if summary.entropy_milli() > peak_entropy {
peak_entropy = summary.entropy_milli()
}
let difference = pixel_difference(previous, current)
if difference > 0 {
active += 1
}
changed += difference
previous = current
}
AnimationComplexity::{
frame_count: self.frames.length(),
total_pixels,
changed_pixels: changed,
mean_unique_colors: if self.frames.length() == 0 {
0
} else {
unique_total / self.frames.length()
},
active_frames: active,
peak_entropy,
}
}
///|
/// Return an indexed color frequency table sorted by descending frequency.
pub fn Canvas::color_frequencies(
self : Canvas,
limit : Int,
) -> Array[(Rgba, Int)] {
let histogram : Map[Int, (Rgba, Int)] = Map([])
for color in self.pixels {
let key = color.packed_rgb()
match histogram.get(key) {
None => histogram[key] = (color, 1)
Some((stored, count)) => histogram[key] = (stored, count + 1)
}
}
let result : Array[(Rgba, Int)] = []
for _, value in histogram {
result.push(value)
}
let safe_limit = if limit < 0 { 0 } else { limit }
while result.length() > safe_limit {
let mut smallest = 0
for i in 1.. Int {
let threshold = if minimum_count < 1 { 1 } else { minimum_count }
let mut count = 0
for pair in self.color_frequencies(1000000) {
if pair.1 >= threshold {
count += 1
}
}
count
}
///|
/// Return a normalized activity score in the range 0..1000.
pub fn Animation::activity_per_mille(self : Animation) -> Int {
let complexity = self.complexity()
let total = complexity.total_pixels()
if total <= 0 {
0
} else {
let value = complexity.changed_pixels() * 1000 / total
if value > 1000 {
1000
} else {
value
}
}
}