package com.xenoage.zong.renderer.slur; import static com.xenoage.utils.collections.CollectionUtils.alist; import java.util.List; import lombok.Getter; import com.xenoage.utils.math.geom.Point2f; import com.xenoage.zong.symbols.path.CubicCurveTo; import com.xenoage.zong.symbols.path.MoveTo; import com.xenoage.zong.symbols.path.Path; import com.xenoage.zong.symbols.path.PathElement; /** * Shape of a slur, drawn in the Default style. * * This is a slur which is thicker in the middle * and has sharp ends. * * @author Andreas Wenger */ public class SimpleSlurShape { //TODO (for more complicated slurs like S-slurs): different algorithms for different symbol pools. //for example printed style bounding quads look like this (because vertical lines are thinner than horizontal ones) // ______ // __--| |--___ // | |______| | | // |__-- --|___| //(this is the DefaultShapeStrategy) // //while handwritten style bounding quads look more than this (because they have equal line width everywhere). // _______ // ___--\ /--____ // \ \ |_____| / / // \___\-- -- /___/ public final float interlineSpace; public final Point2f p1top, p2top, c1top, c2top, p1bottom, p2bottom, c1bottom, c2bottom; @Getter private Path path; /** * Creates the shape of a slur, using the given Bézier curve. * @param p1 the starting point in mm * @param p2 the ending point in mm * @param c1 the first control point in mm * @param c2 the second control point in mm * @param interlineSpace the interline space in mm */ public SimpleSlurShape(Point2f p1, Point2f p2, Point2f c1, Point2f c2, float interlineSpace) { //TODO: the following is only a rough estimate, not an exact formula!! //we want the slur to have a height of only about 30% to 40% in the middle, //but the following is no formula to compute that in a correct way. this.interlineSpace = interlineSpace; //height at the end points float startHeight = 0.1f * interlineSpace; //maximum width in the middle: 0.3 interline spaces float maxHeight = 0.3f * interlineSpace; float s = startHeight / 2; float m = maxHeight / 2; this.p1top = p1.add(0, s); this.p2top = p2.add(0, s); this.c1top = c1.add(0, s + m); this.c2top = c2.add(0, s + m); this.p1bottom = p1.add(0, -s); this.p2bottom = p2.add(0, -s); this.c1bottom = c1.add(0, -(s + m)); this.c2bottom = c2.add(0, -(s + m)); this.path = createPath(); } private Path createPath() { List<PathElement> elements = alist(5); float cap = interlineSpace / 4; elements.add(new MoveTo(p1top)); //bezier curve from p1top to p2top elements.add(new CubicCurveTo(c1top, c2top, p2top)); //cap at p2 Point2f capDir = p2top.sub(c2top).normalize().scale(cap); elements.add(new CubicCurveTo(p2top.add(capDir), p2bottom.add(capDir), p2bottom)); //bezier curve back from p2bottom to p1bottom elements.add(new CubicCurveTo(c2bottom, c1bottom, p1bottom)); //cap at p1 capDir = p1top.sub(c1top).normalize().scale(cap); elements.add(new CubicCurveTo(p1bottom.add(capDir), p1top.add(capDir), p1top)); Path path = new Path(elements); return path; } }