mirror of
https://github.com/servo/servo.git
synced 2025-08-06 22:15:33 +01:00
Move decompose and recompose functions to impl and implement interpolations
This commit is contained in:
parent
10b631dfd1
commit
5ee93b28cb
1 changed files with 177 additions and 125 deletions
|
@ -21,7 +21,6 @@ use properties::longhands::z_index::computed_value::T as ZIndex;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use super::ComputedValues;
|
use super::ComputedValues;
|
||||||
use values::CSSFloat;
|
|
||||||
use values::computed::{Angle, LengthOrPercentageOrAuto, LengthOrPercentageOrNone};
|
use values::computed::{Angle, LengthOrPercentageOrAuto, LengthOrPercentageOrNone};
|
||||||
use values::computed::{BorderRadiusSize, LengthOrNone};
|
use values::computed::{BorderRadiusSize, LengthOrNone};
|
||||||
use values::computed::{CalcLengthOrPercentage, LengthOrPercentage};
|
use values::computed::{CalcLengthOrPercentage, LengthOrPercentage};
|
||||||
|
@ -624,6 +623,8 @@ impl Interpolate for LengthOrNone {
|
||||||
use properties::longhands::transform::computed_value::ComputedMatrix;
|
use properties::longhands::transform::computed_value::ComputedMatrix;
|
||||||
use properties::longhands::transform::computed_value::ComputedOperation as TransformOperation;
|
use properties::longhands::transform::computed_value::ComputedOperation as TransformOperation;
|
||||||
use properties::longhands::transform::computed_value::T as TransformList;
|
use properties::longhands::transform::computed_value::T as TransformList;
|
||||||
|
use values::CSSFloat;
|
||||||
|
use values::specified::Angle as SpecifiedAngle;
|
||||||
|
|
||||||
/// Check if it's possible to do a direct numerical interpolation
|
/// Check if it's possible to do a direct numerical interpolation
|
||||||
/// between these two transform lists.
|
/// between these two transform lists.
|
||||||
|
@ -702,8 +703,9 @@ impl Interpolate for LengthOrNone {
|
||||||
match (from, to) {
|
match (from, to) {
|
||||||
(&TransformOperation::Matrix(from),
|
(&TransformOperation::Matrix(from),
|
||||||
&TransformOperation::Matrix(_to)) => {
|
&TransformOperation::Matrix(_to)) => {
|
||||||
// TODO(gw): Implement matrix decomposition and interpolation
|
// TODO: It doesn't yet handle the case where one of the matrices are not 2D
|
||||||
result.push(TransformOperation::Matrix(from));
|
let interpolated = from.interpolate(&_to, time).unwrap();
|
||||||
|
result.push(TransformOperation::Matrix(interpolated));
|
||||||
}
|
}
|
||||||
(&TransformOperation::Skew(fx, fy),
|
(&TransformOperation::Skew(fx, fy),
|
||||||
&TransformOperation::Skew(tx, ty)) => {
|
&TransformOperation::Skew(tx, ty)) => {
|
||||||
|
@ -735,14 +737,21 @@ impl Interpolate for LengthOrNone {
|
||||||
let ia = fa.interpolate(&ta, time).unwrap();
|
let ia = fa.interpolate(&ta, time).unwrap();
|
||||||
result.push(TransformOperation::Rotate(fx, fy, fz, ia));
|
result.push(TransformOperation::Rotate(fx, fy, fz, ia));
|
||||||
} else {
|
} else {
|
||||||
// TODO(gw): Implement matrix decomposition and interpolation
|
let matrix_f = rotate_to_matrix(fx, fy, fz, fa);
|
||||||
result.push(TransformOperation::Rotate(fx, fy, fz, fa));
|
let matrix_t = rotate_to_matrix(tx, ty, tz, ta);
|
||||||
|
let interpolated = matrix_f.interpolate(&matrix_t, time).unwrap();
|
||||||
|
|
||||||
|
result.push(TransformOperation::Matrix(interpolated));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(&TransformOperation::Perspective(fd),
|
(&TransformOperation::Perspective(fd),
|
||||||
&TransformOperation::Perspective(_td)) => {
|
&TransformOperation::Perspective(_td)) => {
|
||||||
// TODO(gw): Implement matrix decomposition and interpolation
|
let mut fd_matrix = ComputedMatrix::identity();
|
||||||
result.push(TransformOperation::Perspective(fd));
|
let mut td_matrix = ComputedMatrix::identity();
|
||||||
|
fd_matrix.m43 = -1. / fd.to_f32_px();
|
||||||
|
td_matrix.m43 = -1. / _td.to_f32_px();
|
||||||
|
let interpolated = fd_matrix.interpolate(&td_matrix, time).unwrap();
|
||||||
|
result.push(TransformOperation::Matrix(interpolated));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// This should be unreachable due to the can_interpolate_list() call.
|
// This should be unreachable due to the can_interpolate_list() call.
|
||||||
|
@ -758,9 +767,38 @@ impl Interpolate for LengthOrNone {
|
||||||
TransformList(Some(result))
|
TransformList(Some(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// https://drafts.csswg.org/css-transforms/#Rotate3dDefined
|
||||||
|
fn rotate_to_matrix(x: f32, y: f32, z: f32, a: SpecifiedAngle) -> ComputedMatrix {
|
||||||
|
let rad = a.radians();
|
||||||
|
let sc = (rad / 2.0).sin() * (rad / 2.0).cos();
|
||||||
|
let sq = 1.0 / 2.0 * (1.0 - (rad).cos());
|
||||||
|
|
||||||
|
ComputedMatrix {
|
||||||
|
m11: 1.0 - 2.0 * (y * y + z * z) * sq,
|
||||||
|
m12: 2.0 * (x * y * sq - z * sc),
|
||||||
|
m13: 2.0 * (x * z * sq + y * sc),
|
||||||
|
m14: 0.0,
|
||||||
|
|
||||||
|
m21: 2.0 * (x * y * sq + z * sc),
|
||||||
|
m22: 1.0 - 2.0 * (x * x + z * z) * sq,
|
||||||
|
m23: 2.0 * (y * z * sq - x * sc),
|
||||||
|
m24: 0.0,
|
||||||
|
|
||||||
|
m31: 2.0 * (x * z * sq - y * sc),
|
||||||
|
m32: 2.0 * (y * z * sq + x * sc),
|
||||||
|
m33: 1.0 - 2.0 * (x * x + y * y) * sq,
|
||||||
|
|
||||||
|
m34: 0.0,
|
||||||
|
m41: 0.0,
|
||||||
|
m42: 0.0,
|
||||||
|
m43: 0.0,
|
||||||
|
m44: 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
|
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
|
||||||
pub struct DecomposedMatrix {
|
pub struct InnerMatrix2D {
|
||||||
pub m11: CSSFloat, pub m12: CSSFloat,
|
pub m11: CSSFloat, pub m12: CSSFloat,
|
||||||
pub m21: CSSFloat, pub m22: CSSFloat,
|
pub m21: CSSFloat, pub m22: CSSFloat,
|
||||||
}
|
}
|
||||||
|
@ -779,16 +817,16 @@ impl Interpolate for LengthOrNone {
|
||||||
pub translate: Translate2D,
|
pub translate: Translate2D,
|
||||||
pub scale: Scale2D,
|
pub scale: Scale2D,
|
||||||
pub angle: f32,
|
pub angle: f32,
|
||||||
pub matrix: DecomposedMatrix,
|
pub matrix: InnerMatrix2D,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Interpolate for DecomposedMatrix {
|
impl Interpolate for InnerMatrix2D {
|
||||||
fn interpolate(&self, other: &Self, time: f64) -> Result<Self, ()> {
|
fn interpolate(&self, other: &Self, time: f64) -> Result<Self, ()> {
|
||||||
Ok(DecomposedMatrix {
|
Ok(InnerMatrix2D {
|
||||||
m11: self.m11.interpolate(&other.m11, time).unwrap(),
|
m11: try!(self.m11.interpolate(&other.m11, time)),
|
||||||
m12: self.m12.interpolate(&other.m12, time).unwrap(),
|
m12: try!(self.m12.interpolate(&other.m12, time)),
|
||||||
m21: self.m21.interpolate(&other.m21, time).unwrap(),
|
m21: try!(self.m21.interpolate(&other.m21, time)),
|
||||||
m22: self.m22.interpolate(&other.m22, time).unwrap(),
|
m22: try!(self.m22.interpolate(&other.m22, time)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -796,8 +834,8 @@ impl Interpolate for LengthOrNone {
|
||||||
impl Interpolate for Translate2D {
|
impl Interpolate for Translate2D {
|
||||||
fn interpolate(&self, other: &Self, time: f64) -> Result<Self, ()> {
|
fn interpolate(&self, other: &Self, time: f64) -> Result<Self, ()> {
|
||||||
Ok(Translate2D(
|
Ok(Translate2D(
|
||||||
self.0.interpolate(&other.0, time).unwrap(),
|
try!(self.0.interpolate(&other.0, time)),
|
||||||
self.1.interpolate(&other.1, time).unwrap()
|
try!(self.1.interpolate(&other.1, time))
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -805,8 +843,8 @@ impl Interpolate for LengthOrNone {
|
||||||
impl Interpolate for Scale2D {
|
impl Interpolate for Scale2D {
|
||||||
fn interpolate(&self, other: &Self, time: f64) -> Result<Self, ()> {
|
fn interpolate(&self, other: &Self, time: f64) -> Result<Self, ()> {
|
||||||
Ok(Scale2D(
|
Ok(Scale2D(
|
||||||
self.0.interpolate(&other.0, time).unwrap(),
|
try!(self.0.interpolate(&other.0, time)),
|
||||||
self.1.interpolate(&other.1, time).unwrap()
|
try!(self.1.interpolate(&other.1, time))
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -816,9 +854,9 @@ impl Interpolate for LengthOrNone {
|
||||||
fn interpolate(&self, other: &Self, time: f64) -> Result<Self, ()> {
|
fn interpolate(&self, other: &Self, time: f64) -> Result<Self, ()> {
|
||||||
// If x-axis of one is flipped, and y-axis of the other,
|
// If x-axis of one is flipped, and y-axis of the other,
|
||||||
// convert to an unflipped rotation.
|
// convert to an unflipped rotation.
|
||||||
let mut scale = self.scale.clone();
|
let mut scale = self.scale;
|
||||||
let mut angle = self.angle.clone();
|
let mut angle = self.angle;
|
||||||
let mut other_angle = other.angle.clone();
|
let mut other_angle = other.angle;
|
||||||
if (scale.0 < 0.0 && other.scale.1 < 0.0) || (scale.1 < 0.0 && other.scale.0 < 0.0) {
|
if (scale.0 < 0.0 && other.scale.1 < 0.0) || (scale.1 < 0.0 && other.scale.0 < 0.0) {
|
||||||
scale.0 = -scale.0;
|
scale.0 = -scale.0;
|
||||||
scale.1 = -scale.1;
|
scale.1 = -scale.1;
|
||||||
|
@ -843,124 +881,138 @@ impl Interpolate for LengthOrNone {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interpolate all values.
|
// Interpolate all values.
|
||||||
let translate = self.translate.interpolate(&other.translate, time);
|
let translate = try!(self.translate.interpolate(&other.translate, time));
|
||||||
let scale = scale.interpolate(&other.scale, time);
|
let scale = try!(scale.interpolate(&other.scale, time));
|
||||||
let angle = angle.interpolate(&other_angle, time);
|
let angle = try!(angle.interpolate(&other_angle, time));
|
||||||
let matrix = self.matrix.interpolate(&other.matrix, time);
|
let matrix = try!(self.matrix.interpolate(&other.matrix, time));
|
||||||
|
|
||||||
Ok(MatrixDecomposed2D {
|
Ok(MatrixDecomposed2D {
|
||||||
translate: translate.unwrap(),
|
translate: translate,
|
||||||
scale: scale.unwrap(),
|
scale: scale,
|
||||||
angle: angle.unwrap(),
|
angle: angle,
|
||||||
matrix: matrix.unwrap(),
|
matrix: matrix,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decompose a matrix.
|
impl Interpolate for ComputedMatrix {
|
||||||
/// https://drafts.csswg.org/css-transforms/#decomposing-a-2d-matrix
|
fn interpolate(&self, other: &Self, time: f64) -> Result<Self, ()> {
|
||||||
fn decompose_matrix(matrix: ComputedMatrix) -> MatrixDecomposed2D {
|
let decomposed_from = MatrixDecomposed2D::from(*self);
|
||||||
let mut row0x = matrix.m11;
|
let decomposed_to = MatrixDecomposed2D::from(*other);
|
||||||
let mut row0y = matrix.m12;
|
let interpolated = try!(decomposed_from.interpolate(&decomposed_to, time));
|
||||||
let mut row1x = matrix.m21;
|
Ok(ComputedMatrix::from(interpolated))
|
||||||
let mut row1y = matrix.m22;
|
|
||||||
|
|
||||||
let translate = Translate2D(matrix.m41, matrix.m42);
|
|
||||||
let mut scale = Scale2D((row0x * row0x + row0y * row0y).sqrt(),
|
|
||||||
(row1x * row1x + row1y * row1y).sqrt());
|
|
||||||
|
|
||||||
// If determinant is negative, one axis was flipped.
|
|
||||||
let determinant = row0x * row1y - row0y * row1x;
|
|
||||||
if determinant < 0. {
|
|
||||||
if row0x < row1y {
|
|
||||||
scale.0 = -scale.0;
|
|
||||||
} else {
|
|
||||||
scale.1 = -scale.1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Renormalize matrix to remove scale.
|
|
||||||
if scale.0 != 0.0 {
|
|
||||||
row0x *= 1. / scale.0;
|
|
||||||
row0y *= 1. / scale.0;
|
|
||||||
}
|
|
||||||
if scale.1 != 0.0 {
|
|
||||||
row1x *= 1. / scale.1;
|
|
||||||
row1y *= 1. / scale.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute rotation and renormalize matrix.
|
|
||||||
let mut angle = row0y.atan2(row0x);
|
|
||||||
if angle != 0.0 {
|
|
||||||
let sn = -row0y;
|
|
||||||
let cs = row0x;
|
|
||||||
let m11 = row0x;
|
|
||||||
let m12 = row0y;
|
|
||||||
let m21 = row1x;
|
|
||||||
let m22 = row1y;
|
|
||||||
row0x = cs * m11 + sn * m21;
|
|
||||||
row0y = cs * m12 + sn * m22;
|
|
||||||
row1x = -sn * m11 + cs * m21;
|
|
||||||
row1y = -sn * m12 + cs * m22;
|
|
||||||
}
|
|
||||||
|
|
||||||
let m = DecomposedMatrix {
|
|
||||||
m11: row0x, m12: row0y,
|
|
||||||
m21: row1x, m22: row1y,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert into degrees because our rotation functions expect it.
|
|
||||||
angle = angle.to_degrees();
|
|
||||||
MatrixDecomposed2D {
|
|
||||||
translate: translate,
|
|
||||||
scale: scale,
|
|
||||||
angle: angle,
|
|
||||||
matrix: m,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// https://drafts.csswg.org/css-transforms/#recomposing-to-a-2d-matrix
|
impl From<ComputedMatrix> for MatrixDecomposed2D {
|
||||||
fn recompose_matrix(decomposed: MatrixDecomposed2D) -> ComputedMatrix {
|
/// Decompose a matrix.
|
||||||
let mut computed_matrix = ComputedMatrix::identity();
|
/// https://drafts.csswg.org/css-transforms/#decomposing-a-2d-matrix
|
||||||
computed_matrix.m11 = decomposed.matrix.m11;
|
fn from(matrix: ComputedMatrix) -> MatrixDecomposed2D {
|
||||||
computed_matrix.m12 = decomposed.matrix.m12;
|
let mut row0x = matrix.m11;
|
||||||
computed_matrix.m21 = decomposed.matrix.m21;
|
let mut row0y = matrix.m12;
|
||||||
computed_matrix.m22 = decomposed.matrix.m22;
|
let mut row1x = matrix.m21;
|
||||||
|
let mut row1y = matrix.m22;
|
||||||
|
|
||||||
// Translate matrix.
|
let translate = Translate2D(matrix.m41, matrix.m42);
|
||||||
computed_matrix.m41 = decomposed.translate.0 * decomposed.matrix.m11 +
|
let mut scale = Scale2D((row0x * row0x + row0y * row0y).sqrt(),
|
||||||
decomposed.translate.1 * decomposed.matrix.m21;
|
(row1x * row1x + row1y * row1y).sqrt());
|
||||||
computed_matrix.m42 = decomposed.translate.0 * decomposed.matrix.m11 +
|
|
||||||
decomposed.translate.1 * decomposed.matrix.m21;
|
|
||||||
|
|
||||||
// Rotate matrix.
|
// If determinant is negative, one axis was flipped.
|
||||||
let angle = decomposed.angle.to_radians();
|
let determinant = row0x * row1y - row0y * row1x;
|
||||||
let cos_angle = angle.cos();
|
if determinant < 0. {
|
||||||
let sin_angle = angle.sin();
|
if row0x < row1y {
|
||||||
|
scale.0 = -scale.0;
|
||||||
|
} else {
|
||||||
|
scale.1 = -scale.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut rotate_matrix = ComputedMatrix::identity();
|
// Renormalize matrix to remove scale.
|
||||||
rotate_matrix.m11 = cos_angle;
|
if scale.0 != 0.0 {
|
||||||
rotate_matrix.m12 = sin_angle;
|
row0x *= 1. / scale.0;
|
||||||
rotate_matrix.m21 = -sin_angle;
|
row0y *= 1. / scale.0;
|
||||||
rotate_matrix.m22 = cos_angle;
|
}
|
||||||
|
if scale.1 != 0.0 {
|
||||||
|
row1x *= 1. / scale.1;
|
||||||
|
row1y *= 1. / scale.1;
|
||||||
|
}
|
||||||
|
|
||||||
let matrix_clone = computed_matrix.clone();
|
// Compute rotation and renormalize matrix.
|
||||||
// Multiplication of computed_matrix and rotate_matrix
|
let mut angle = row0y.atan2(row0x);
|
||||||
% for i in range(1, 5):
|
if angle != 0.0 {
|
||||||
% for j in range(1, 5):
|
let sn = -row0y;
|
||||||
computed_matrix.m${i}${j} = (matrix_clone.m${i}1 * rotate_matrix.m1${j}) +
|
let cs = row0x;
|
||||||
(matrix_clone.m${i}2 *rotate_matrix.m2${j}) +
|
let m11 = row0x;
|
||||||
(matrix_clone.m${i}3 * rotate_matrix.m3${j}) +
|
let m12 = row0y;
|
||||||
(matrix_clone.m${i}4 * rotate_matrix.m4${j});
|
let m21 = row1x;
|
||||||
|
let m22 = row1y;
|
||||||
|
row0x = cs * m11 + sn * m21;
|
||||||
|
row0y = cs * m12 + sn * m22;
|
||||||
|
row1x = -sn * m11 + cs * m21;
|
||||||
|
row1y = -sn * m12 + cs * m22;
|
||||||
|
}
|
||||||
|
|
||||||
|
let m = InnerMatrix2D {
|
||||||
|
m11: row0x, m12: row0y,
|
||||||
|
m21: row1x, m22: row1y,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert into degrees because our rotation functions expect it.
|
||||||
|
angle = angle.to_degrees();
|
||||||
|
MatrixDecomposed2D {
|
||||||
|
translate: translate,
|
||||||
|
scale: scale,
|
||||||
|
angle: angle,
|
||||||
|
matrix: m,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MatrixDecomposed2D> for ComputedMatrix {
|
||||||
|
/// Recompose a matrix.
|
||||||
|
/// https://drafts.csswg.org/css-transforms/#recomposing-to-a-2d-matrix
|
||||||
|
fn from(decomposed: MatrixDecomposed2D) -> ComputedMatrix {
|
||||||
|
let mut computed_matrix = ComputedMatrix::identity();
|
||||||
|
computed_matrix.m11 = decomposed.matrix.m11;
|
||||||
|
computed_matrix.m12 = decomposed.matrix.m12;
|
||||||
|
computed_matrix.m21 = decomposed.matrix.m21;
|
||||||
|
computed_matrix.m22 = decomposed.matrix.m22;
|
||||||
|
|
||||||
|
// Translate matrix.
|
||||||
|
computed_matrix.m41 = decomposed.translate.0 * decomposed.matrix.m11 +
|
||||||
|
decomposed.translate.1 * decomposed.matrix.m21;
|
||||||
|
computed_matrix.m42 = decomposed.translate.0 * decomposed.matrix.m12 +
|
||||||
|
decomposed.translate.1 * decomposed.matrix.m22;
|
||||||
|
|
||||||
|
// Rotate matrix.
|
||||||
|
let angle = decomposed.angle.to_radians();
|
||||||
|
let cos_angle = angle.cos();
|
||||||
|
let sin_angle = angle.sin();
|
||||||
|
|
||||||
|
let mut rotate_matrix = ComputedMatrix::identity();
|
||||||
|
rotate_matrix.m11 = cos_angle;
|
||||||
|
rotate_matrix.m12 = sin_angle;
|
||||||
|
rotate_matrix.m21 = -sin_angle;
|
||||||
|
rotate_matrix.m22 = cos_angle;
|
||||||
|
|
||||||
|
let matrix_clone = computed_matrix;
|
||||||
|
// Multiplication of computed_matrix and rotate_matrix
|
||||||
|
% for i in range(1, 5):
|
||||||
|
% for j in range(1, 5):
|
||||||
|
computed_matrix.m${i}${j} = (matrix_clone.m${i}1 * rotate_matrix.m1${j}) +
|
||||||
|
(matrix_clone.m${i}2 * rotate_matrix.m2${j}) +
|
||||||
|
(matrix_clone.m${i}3 * rotate_matrix.m3${j}) +
|
||||||
|
(matrix_clone.m${i}4 * rotate_matrix.m4${j});
|
||||||
|
% endfor
|
||||||
% endfor
|
% endfor
|
||||||
% endfor
|
|
||||||
|
|
||||||
// Scale matrix.
|
// Scale matrix.
|
||||||
computed_matrix.m11 *= decomposed.scale.0;
|
computed_matrix.m11 *= decomposed.scale.0;
|
||||||
computed_matrix.m12 *= decomposed.scale.0;
|
computed_matrix.m12 *= decomposed.scale.0;
|
||||||
computed_matrix.m21 *= decomposed.scale.1;
|
computed_matrix.m21 *= decomposed.scale.1;
|
||||||
computed_matrix.m22 *= decomposed.scale.1;
|
computed_matrix.m22 *= decomposed.scale.1;
|
||||||
computed_matrix
|
computed_matrix
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// https://drafts.csswg.org/css-transforms/#interpolation-of-transforms
|
/// https://drafts.csswg.org/css-transforms/#interpolation-of-transforms
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue