All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jaudiolibs.pipes.graph.Property Maven / Gradle / Ivy

Go to download

Support for building and running graphs of Pipes UGens, based on the audio API inside PraxisCORE.

The newest version!
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2019 Neil C Smith.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License version 3
 * along with this work; if not, see http://www.gnu.org/licenses/
 *
 */
package org.jaudiolibs.pipes.graph;

import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import java.util.function.DoubleConsumer;

/**
 * A wrapper type for double values that supports key frame animation in sync
 * with the {@link Graph}. This type partly parallels the same named type in the
 * PraxisLIVE (audio) API.
 * 

* Property fields may be annotated with {@link Inject}. */ public final class Property implements Graph.Dependent { private static final long TO_NANO = 1000_000_000; private final List links; private Animator animator; private double value; private Graph graph; public Property() { this.links = new CopyOnWriteArrayList<>(); } /** * Set the value of this property. This will cancel any active animation, * and notify any attached links. * * @param value * @return this */ public Property set(double value) { finishAnimating(); setImpl(value); return this; } /** * Return the current value. * * @return value */ public double get() { return value; } /** * Call the provided consumer with the double value whenever the value * changes. This is a shorthand for {@code values().link(consumer);}. The * double value will be as if calling {@link #getDouble()}. * * @param consumer double consumer * @return this */ public Property link(DoubleConsumer consumer) { Link dl = new Link(); dl.link(consumer); return this; } /** * Return a new {@link Linkable.Double} for observing changing values. The * double value will be as if calling {@link #get()}. * * @return Linkable.Double of values. */ public Linkable.Double values() { return new Link(); } /** * Clear all Linkables from the Property. * * @return this */ public Property clearLinks() { links.clear(); return this; } /** * Return the Animator for the Property, creating it if necessary. * * @return property animator */ public Animator animator() { if (animator == null) { animator = new Animator(this); } return animator; } /** * Animate the property value to the provided values. This is a shorthand * for calling {@code animator().to(...)}. *

* This method returns the animator so that you can chain calls, eg. * {@code prop.to(1, 0).in(1, 0.25).easeInOut();} * * @return property animator */ public Property.Animator to(double... to) { return animator().to(to); } @Override public void attach(Graph graph) { this.graph = graph; } @Override public void detach(Graph graph) { finishAnimating(); this.graph = null; } @Override public void update() { if (animator != null) { animator.tick(); } } /** * Return whether the property is currently animating. * * @return property animator active */ public boolean isAnimating() { return animator != null && animator.isAnimating(); } private void setImpl(double value) { this.value = value; updateLinks(); } private void finishAnimating() { if (animator != null) { animator.stop(); } } private void updateLinks() { links.forEach(l -> l.fire(value)); } private class Link implements Linkable.Double { private DoubleConsumer consumer; @Override public void link(DoubleConsumer consumer) { if (this.consumer != null) { throw new IllegalStateException("Cannot link multiple consumers in one chain"); } this.consumer = Objects.requireNonNull(consumer); fire(get()); links.add(this); } private void fire(double value) { consumer.accept(value); } } /** * Provides keyframe animation support for Property. Methods return this so * that they can be chained - eg. {@code to(1, 0).in(1, 0.25).easeInOut()} */ public static class Animator { private final static long[] DEFAULT_IN = new long[]{0}; private final static Easing[] DEFAULT_EASING = new Easing[]{Easing.linear}; private Property property; int index; private double[] to; private long[] in; private Easing[] easing; private double fromValue; private long fromTime; private boolean animating; private Consumer onDoneConsumer; private long overrun; private Animator(Property p) { this.property = p; in = DEFAULT_IN; easing = DEFAULT_EASING; } private void attach(Property p) { this.property = p; } /** * Set the target values for animation and start animation. The number * of values provided to this method controls the number of keyframes. * * @param to target values * @return this */ public Animator to(double... to) { if (to.length < 1) { throw new IllegalArgumentException(); } this.to = to; index = 0; in = DEFAULT_IN; easing = DEFAULT_EASING; fromValue = property.value; fromTime = property.graph.nanos(); if (!animating) { animating = true; } return this; } /** * Set the time in seconds for each keyframe. The number of provided * values may be different than the number of keyframes passed to to(). * Values will be cycled through as needed. *

* eg. {@code to(100, 50, 250).in(1, 0.5)} is the same as * {@code to(100, 50, 250).in(1, 0.5, 1)} * * @param in times in seconds * @return this */ public Animator in(double... in) { if (in.length < 1) { this.in = DEFAULT_IN; } else { this.in = new long[in.length]; for (int i = 0; i < in.length; i++) { this.in[i] = (long) (in[i] * TO_NANO); } this.in[0] = Math.max(0, this.in[0] - overrun); } return this; } /** * Set the easing mode for each keyframe. The number of provided values * may be different than the number of keyframes passed to to(). Values * will be cycled through as needed. * * @param easing easing mode to use * @return this */ public Animator easing(Easing... easing) { if (easing.length < 1) { this.easing = DEFAULT_EASING; } else { this.easing = easing; } return this; } /** * Convenience method to use {@link Easing#linear} easing for all * keyframes. * * @return this */ public Animator linear() { return easing(Easing.linear); } /** * Convenience method to use {@link Easing#ease} easing for all * keyframes. * * @return this */ public Animator ease() { return easing(Easing.ease); } /** * Convenience method to use {@link Easing#easeIn} easing for all * keyframes. * * @return this */ public Animator easeIn() { return easing(Easing.easeIn); } /** * Convenience method to use {@link Easing#easeOut} easing for all * keyframes. * * @return this */ public Animator easeOut() { return easing(Easing.easeOut); } /** * Convenience method to use {@link Easing#easeInOut} easing for all * keyframes. * * @return this */ public Animator easeInOut() { return easing(Easing.easeInOut); } /** * Stop animating. The current property value will be retained. * * @return this */ public Animator stop() { index = 0; animating = false; return this; } /** * Whether an animation is currently active. * * @return animation active */ public boolean isAnimating() { return animating; } /** * Set a consumer to be called each time the Animator finishes * animation. Also calls the consumer immediately if no animation is * currently active. *

* Unlike restarting an animation by polling isAnimating(), an animation * started inside this consumer will take into account any time overrun * between the target and actual finish time of the completing * animation. * * @param whenDoneConsumer function to call * @return this */ public Animator whenDone(Consumer whenDoneConsumer) { this.onDoneConsumer = whenDoneConsumer; if (!animating) { onDoneConsumer.accept(property); } return this; } private void tick() { if (!animating) { assert false; return; } try { long currentTime = property.graph.nanos(); double toValue = to[index]; long duration = in[index % in.length]; double proportion; if (duration < 1) { proportion = 1; } else { proportion = (currentTime - fromTime) / (double) duration; } if (proportion >= 1) { index++; if (index >= to.length) { finish(currentTime - (fromTime + duration)); } else { fromValue = toValue; fromTime += duration; } property.setImpl(toValue); } else if (proportion > 0) { Easing ease = easing[index % easing.length]; double d = ease.calculate(proportion); d = (d * (toValue - fromValue)) + fromValue; property.setImpl(d); } else { // p.setImpl(fromTime, fromValue); ??? } } catch (Exception exception) { finish(0); } } private void finish(long overrun) { index = 0; animating = false; this.overrun = overrun; if (onDoneConsumer != null) { onDoneConsumer.accept(property); } this.overrun = 0; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy