Harbor

branch main
showing the latest snapshot on main
variations.odin 6.3 KB · Plain text
variations.odin 0644 Raw
package font

// ============================================================================
// VARIABLE FONTS (fvar/avar)
// ============================================================================

// A variation axis defined in the fvar table
Variation_Axis :: struct {
	tag:       [4]u8,   // 4-byte axis tag (e.g. "wght", "wdth", "ital")
	min_value: f32,
	def_value: f32,
	max_value: f32,
	name_id:   u16,
	hidden:    bool,
}

// A named instance defined in the fvar table
Named_Instance :: struct {
	name_id: u16,
	coords:  [8]f32,  // normalized coordinates (up to 8 axes)
}

// Get the number of variation axes in this font. Returns 0 for non-variable fonts.
get_variation_axis_count :: proc(info: ^Font_Info) -> i32 {
	fvar := find_table(info.data, u32(info.fontstart), "fvar")
	if fvar == 0 do return 0

	data := info.data[fvar:]
	version := ttULONG(data)
	if version != 0x00010000 do return 0

	return i32(ttUSHORT(data[8:]))
}

// Get information about a specific variation axis. Returns false if index is out of range.
get_variation_axis :: proc(info: ^Font_Info, axis_index: i32, axis: ^Variation_Axis) -> bool {
	fvar := find_table(info.data, u32(info.fontstart), "fvar")
	if fvar == 0 do return false

	data := info.data[fvar:]
	version := ttULONG(data)
	if version != 0x00010000 do return false

	axes_offset := u32(ttUSHORT(data[4:]))
	axis_count := i32(ttUSHORT(data[8:]))
	axis_size := i32(ttUSHORT(data[10:]))

	if axis_index < 0 || axis_index >= axis_count do return false

	rec := data[axes_offset + u32(axis_index) * u32(axis_size):]

	axis.tag[0] = rec[0]
	axis.tag[1] = rec[1]
	axis.tag[2] = rec[2]
	axis.tag[3] = rec[3]
	axis.min_value = fixed_to_f32(ttULONG(rec[4:]))
	axis.def_value = fixed_to_f32(ttULONG(rec[8:]))
	axis.max_value = fixed_to_f32(ttULONG(rec[12:]))
	flags := ttUSHORT(rec[16:])
	axis.name_id = ttUSHORT(rec[18:])
	axis.hidden = (flags >> 3) & 1 == 1

	// Clamp to ensure min <= def <= max
	if axis.min_value > axis.def_value do axis.min_value = axis.def_value
	if axis.max_value < axis.def_value do axis.max_value = axis.def_value

	return true
}

// Get all variation axes. Returns the number of axes written.
get_variation_axes :: proc(info: ^Font_Info, axes: []Variation_Axis) -> i32 {
	count := get_variation_axis_count(info)
	n := min(count, i32(len(axes)))
	for i in 0..<n {
		get_variation_axis(info, i, &axes[i])
	}
	return n
}

// Normalize an axis value to the [-1, 1] range.
// Values at the default return 0, values at min return -1, values at max return 1.
normalize_axis_value :: proc(axis: ^Variation_Axis, value: f32) -> f32 {
	v := clamp(value, axis.min_value, axis.max_value)
	if v == axis.def_value {
		return 0
	} else if v < axis.def_value {
		if axis.def_value == axis.min_value do return 0
		return (v - axis.def_value) / (axis.def_value - axis.min_value)
	} else {
		if axis.max_value == axis.def_value do return 0
		return (v - axis.def_value) / (axis.max_value - axis.def_value)
	}
}

// Check if this font is a variable font (has an fvar table with axes)
is_variable_font :: proc(info: ^Font_Info) -> bool {
	return get_variation_axis_count(info) > 0
}

// Get the number of named instances in the font
get_named_instance_count :: proc(info: ^Font_Info) -> i32 {
	fvar := find_table(info.data, u32(info.fontstart), "fvar")
	if fvar == 0 do return 0

	data := info.data[fvar:]
	version := ttULONG(data)
	if version != 0x00010000 do return 0

	return i32(ttUSHORT(data[6:]))
}

// Convert 16.16 fixed-point to f32
@(private)
fixed_to_f32 :: proc(v: u32) -> f32 {
	return f32(i32(v)) / 65536.0
}

// Maximum number of variation axes supported
MAX_VAR_AXES :: 8

// Normalized variation coordinates (one per axis, range [-1, 1])
Var_Coords :: [MAX_VAR_AXES]f32

// Set variation coordinates from user-space axis values (e.g. weight=700).
// Applies fvar normalization and avar remapping.
// Returns the number of axes set.
set_variation :: proc(info: ^Font_Info, coords: ^Var_Coords, tags: []string, values: []f32) -> i32 {
	axis_count := get_variation_axis_count(info)
	if axis_count == 0 do return 0

	// Reset to defaults
	for i in 0..<MAX_VAR_AXES { coords[i] = 0 }

	// For each requested tag, find the axis and normalize
	axes: [MAX_VAR_AXES]Variation_Axis
	get_variation_axes(info, axes[:axis_count])

	for ti in 0..<min(len(tags), len(values)) {
		if len(tags[ti]) != 4 do continue
		for ai in 0..<axis_count {
			a := &axes[ai]
			if a.tag[0] == tags[ti][0] && a.tag[1] == tags[ti][1] &&
			   a.tag[2] == tags[ti][2] && a.tag[3] == tags[ti][3] {
				coords[ai] = normalize_axis_value(a, values[ti])
				break
			}
		}
	}

	// Apply avar remapping
	avar_remap(info, coords, axis_count)

	return axis_count
}

// Apply avar axis remapping to normalized coordinates
@(private)
avar_remap :: proc(info: ^Font_Info, coords: ^Var_Coords, axis_count: i32) {
	avar := find_table(info.data, u32(info.fontstart), "avar")
	if avar == 0 do return

	data := info.data[avar:]
	version := ttULONG(data)
	if version != 0x00010000 do return

	table_axis_count := i32(ttUSHORT(data[6:]))

	// Walk through segment maps (variable-length per axis)
	off: u32 = 8
	n := min(table_axis_count, axis_count)
	for ai in 0..<n {
		pair_count := i32(ttUSHORT(data[off:]))
		off += 2

		if pair_count >= 2 {
			// Convert normalized coord to 2.14 fixed-point for mapping
			value := i16(clamp(coords[ai], -1, 1) * 16384.0)
			mapped := avar_map_value(data[off:], pair_count, value)
			coords[ai] = f32(mapped) / 16384.0
		}

		off += u32(pair_count) * 4 // each pair is 4 bytes (2x i16)
	}
}

@(private)
avar_map_value :: proc(pairs: [^]u8, count: i32, value: i16) -> i16 {
	// pairs are (fromCoord, toCoord) as F2DOT14 (i16)
	from0 := i16(ttUSHORT(pairs[0:]))
	to0 := i16(ttUSHORT(pairs[2:]))

	if value <= from0 {
		return value - from0 + to0
	}

	for i in 1..<count {
		from_i := i16(ttUSHORT(pairs[u32(i) * 4:]))
		to_i := i16(ttUSHORT(pairs[u32(i) * 4 + 2:]))

		if value <= from_i {
			prev_from := i16(ttUSHORT(pairs[u32(i - 1) * 4:]))
			prev_to := i16(ttUSHORT(pairs[u32(i - 1) * 4 + 2:]))

			if prev_from == from_i {
				return prev_to
			}

			// Piecewise linear interpolation
			denom := i32(from_i) - i32(prev_from)
			k := (i32(to_i) - i32(prev_to)) * (i32(value) - i32(prev_from)) + denom / 2
			return i16(i32(prev_to) + k / denom)
		}
	}

	// Beyond last entry
	from_last := i16(ttUSHORT(pairs[u32(count - 1) * 4:]))
	to_last := i16(ttUSHORT(pairs[u32(count - 1) * 4 + 2:]))
	return value - from_last + to_last
}