function ImageMixingTutorial(mode, ms, myimgfile)
% ImageMixingTutorial([mode=1][, ms=200][, myimgfile])
%
% ImageMixingTutorial shows how to use a combination of alpha blending,
% offscreen windows and some basic image processing shaders to mix two
% images together, using a "mix weight mask" (aka alpha mask) which itself
% is dynamically updated via Screen() drawing commands like DrawTexture,
% DrawTexture with shaders, FillRect etc. This allows for interesting
% new gaze contingent displays or dynamically changing binocular rivalry
% stimuli.
%
% The basic working principle:
%
% 1. An offscreen window is created which stores the alpha blend mask
%    with per-pixel mixing weights. ("masktex" in the code).
%
% 2. The offscreen window stores the mix weights in its *luminance* channel,
%    (which is the same as the red channel for technical reasons). This way,
%    grayscale "luminance" values (luminance == red == green == blue) directly
%    encode "mixing weights". As we use a normalized 0-1 color range in this
%    demo ("PsychDefaultSetup(2)"), a grayscale value from 0 - 1 (aka from
%    black to white) directly corresponds to a mix weight from 0 - 1. This
%    allows us to use standard Screen() 2D drawing commands as usual to draw
%    a mix weight mask as a grayscale image into the offscreen window without
%    any deeper knowledge or thought about alpha blending. We can use all
%    drawing commands to quickly and dynamically update or redraw the grayscale
%    image in the offscreen window to create a dynamically changing mix weight
%    mask.
%
% 3. A shader is used to convert the grayscale image in the offscreen window
%    into a alpha mask and draw that alpha mask into the framebuffer of the
%    onscreen window, thereby setting the alpha channel of the onscreen window
%    to the desired mix weight mask for mixing the actual stimulus images.
%
% 4. Alpha blending is used to draw the two target stimulus images, mixing
%    them together according to the alpha channel created in step 3 from the
%    grayscale weight mask dynamically created in step 2.
%
% 5. The final mixed stimulus, e.g., a binocular rivalry stimulus, is shown
%    to the subject, rinse wash, repeat with step 2.
%
% This demo shows how to use normalized color ranges from 0 - 1 as a more
% natural representation of such alpha mix weights. It shows how to use the
% 'WeightedColorComponentSum' shader to both morph up to 4 masks together into
% one weight mask, and as an alternate use, how to move the content of the
% red channel of a window (== luminance/grayscale channel in a grayscale image)
% into the alpha channel, allowing to implement step 3 above. It also uses
% alpha blending in combination with a separate offscreen window in a non-usual
% way to allow to logically separate the process of creating/updating a mix weight
% mask from the process of actually applying that mask to a pair of stimulus images.
% This approach is not neccessary for simple gaze-contingent displays or rivalry
% stimuli (cfe. GazeContingentDemo / GazeContingentTutorial / BubbleDemo for simpler
% approaches). It is beneficial for stimuli which require complex mix masks, or
% complex dynamically updated mix masks, as it allows to implement an approach that
% reduces implementation complexity and is more natural or easier on the brain of
% the implementer of the stimulus, with less potential for coding errors or confusion
% about side effects of alpha blending.
%
% The tutorial allows you to switch between different stages of the processing
% involved in this approach and see their effects "live", by use of different
% keys on the keyboard, and to draw a dynamic mask via use of the mousecursor
% as a paint brush. It also shows some automatically running use of procedural
% shaders, texture animation and other Screen drawing primitives.
%
% This tutorial is powerful in its potential use cases, but requires significant
% customization for specific paradigms, and a good and careful reading of the code.
%
% For a much more simple demo and application of the technique, have a look at
% the SimpleImageMixingDemo.m, written and contributed by Natalia Zaretskaya.
% ___________________________________________________________________

% HISTORY
% 11-Nov-2014 mk   Written.

% Use new-style color specifications in normalized range 0.0 - 1.0:
PsychDefaultSetup(2);

% Setup default mode to color vs. gray.
if nargin < 1
    mode = 1;
end

% Setup default aperture size to 2*200 x 2*200 pixels.
if nargin < 2
    ms=200;
end

% Basepath to our own demo images:
basepath = [ PsychtoolboxRoot 'PsychDemos' filesep ];

% Use default demo images, if no special image was provided.
if nargin < 3
    myimgfile= [basepath 'konijntjes1024x768.jpg'];
end

myblurimgfile= [basepath 'konijntjes1024x768blur.jpg'];
mygrayimgfile= [basepath 'konijntjes1024x768gray.jpg'];

try
    % Set background color to black aka zero intensity:
    backgroundcolor = 0.0;

    % Get the list of screens and choose the one with the highest screen number.
    screenNumber=max(Screen('Screens'));

    % Open a double buffered fullscreen window. Use PsychImaging(), so the
    % normalized 0.0 - 1.0 color format is used for drawing, instead of the
    % old 0 - 255 range:
    [w, wRect] = PsychImaging('OpenWindow', screenNumber, backgroundcolor);

    % Open an offscreen window the same size as the onscreen window. We use
    % this to define the alpha/mixing weight channel used to later mix
    % two images together:
    masktex = Screen('OpenOffscreenWindow', w, [0 0 0 0]);
    DrawFormattedText(masktex, 'Draw something into the mask with the mouse!', 'center', 40, [1 1 1 1]);
    Screen('TextSize', masktex, 256);

    % Load image file:
    fprintf('Using image ''%s''\n', myimgfile);
    imdata=imread(myimgfile);
    imdatablur=imread(myblurimgfile);
    imdatagray=imread(mygrayimgfile);

    % Crop image if it is larger then screen size. There's no image scaling
    % in maketexture:
    [iy, ix, id]=size(imdata);
    [wW, wH]=WindowSize(w);

    if ix>wW || iy>wH
        disp('Image size exceeds screen size');
        disp('Image will be cropped');
    end

    if ix>wW
        cl=round((ix-wW)/2);
        cr=(ix-wW)-cl;
    else
        cl=0;
        cr=0;
    end
    if iy>wH
        ct=round((iy-wH)/2);
        cb=(iy-wH)-ct;
    else
        ct=0;
        cb=0;
    end

    % imdataXXX is the cropped version of the images.
    imdata=imdata(1+ct:iy-cb, 1+cl:ix-cr,:);
    imdatablur=imdatablur(1+ct:iy-cb, 1+cl:ix-cr,:);
    imdatagray=imdatagray(1+ct:iy-cb, 1+cl:ix-cr,:);

    % Compute image for foveated region and periphery:
    switch (mode)
        case 1
            % Mode 1:
            % Fovea contains original image data:
            secondimdata = imdata;
            % Periphery contains grayscale-version:
            firstimdata = imdatagray;
        case 2
            % Fovea contains original image data:
            secondimdata = imdata;
            % Periphery contains blurred-version:
            firstimdata = imdatablur;
        case 3
            % Fovea contains color-inverted image data:
            secondimdata(:,:,:) = 255 - imdata(:,:,:);
            % Periphery contains original data:
            firstimdata = imdata;
        case 4
            % Test-case: One shouldn't see any foveated region on the
            % screen - this is a basic correctness test for blending.
            secondimdata = imdata;
            firstimdata = imdata;
        case 5
            secondimdata = imdata;
            firstimdata = imread([basepath 'PsychExampleExperiments/OldNewRecognition/stims/stim3.jpg']);
        otherwise
            % Unknown mode! We force abortion:
            fprintf('Invalid mode provided!');
            abortthisbeast
    end

    % Build texture for first image:
    firstImage=Screen('MakeTexture', w, firstimdata);

    % Build texture for second image:
    secondImage=Screen('MakeTexture', w, secondimdata);

    % We create a two layers Luminance + Alpha matrix for use as "gaussian" transparency
    % (or mixing weights) brush: Layer 1 (Luminance) is filled with luminance
    % value 1.0 aka white - the ones() function does this nicely for us, by
    % first filling both layers with 1.0:
    [x,y] = meshgrid(-ms:ms, -ms:ms);
    maskblob = ones(2*ms+1, 2*ms+1, 2);

    % Layer 2 (Transparency aka Alpha) is now filled/overwritten with a gaussian
    % transparency/mixing mask.
    xsd = ms / 2.2;
    ysd = ms / 2.2;
    maskblob(:,:,2) = exp(-((x / xsd).^2) - ((y / ysd).^2));

    % Copy alpha to luminance, just for visualization of the "mask brush" later on:
    maskblob(:,:,1) = maskblob(:,:,2);

    % Build a single transparency mask texture:
    gaussbrush = Screen('MakeTexture', w, maskblob);

    % Do initial flip to show blank screen:
    Screen('Flip', w);

    % The mouse-cursor position will define drawing-position. Set cursor
    % initially to center of screen, but do hide it from view:
    [a,b] = RectCenter(wRect);
    SetMouse(a,b,screenNumber);

    % Wait until all keys on keyboard are released:
    KbReleaseWait;

    % Show first image:
    Screen('DrawTexture', w, firstImage);
    Screen('TextSize', w, 24);
    DrawFormattedText(w, 'Step1: Create first texture:\nPress a key to continue\n', 0, 40, 1, 50);
    Screen('Flip', w);

    % Wait for mouseclick:
    KbStrokeWait;

    % Show second image:
    Screen('DrawTexture', w, secondImage);
    Screen('TextSize', w, 24);
    DrawFormattedText(w, 'Step2: Create second texture:\nPress a key to continue\n', 0, 40, 1, 50);
    Screen('Flip', w);
    KbStrokeWait;

    coverage = 0.25;
    mode = 0;
    brushtype = 0;
    startAngle = 0;
    imgRect = CenterRect(Screen('Rect', firstImage), wRect);
    cRect = OffsetRect([0 0 300 300], imgRect(RectLeft), imgRect(RectTop));

    % Build a procedural sine grating texture for a grating with a support of
    % 300 x 300 pixels and a RGBA color offset of 0.5:
    gratingtex = CreateProceduralSineGrating(w, 300, 300, [0.5 0.5 0.5 0.5]);
    gRect = OffsetRect([0 0 300 300], imgRect(RectLeft), imgRect(RectTop) + 300);
    phase = 0;

    % Create a shader that allows to combine the up to four input channels
    % of a texture into a weighted linear combination, using 'DrawTexture's
    % modulateColor parameter to specify the weights. This is used for
    % morphing between up to four alpha-masks, stored in the morphedAlphaTexture.
    minimorphshader = CreateSinglePassImageProcessingShader(w, 'WeightedColorComponentSum');
    
    % Create a texture with the alpha masks we want to morph between.
    %
    % We only create a one channel "luminance" texture which contains a gauss blob.
    % In the following use of this texture we'd like to "morph" between an all-zero layer,
    % an all-one layer and the "maskblob" gaussian shape layer. However, here we can optimize
    % a bit: We don't need an all-zero layer, because we get that implicitely, as any zero
    % value multiplied by any weight will always result in zero ( 0 * x == 0 for any x).
    % A "single layer" luminance texture will store our gaussian blob shape. Now any
    % "single layer luminance texture" automatically gets its alpha channel initialized to a
    % layer of all ones, ie., it implicitely carries around an alpha channel (the 4th channel)
    % which is filled with ones. In practice morphTex will have layers 1-3 (red, green, blue)
    % filled with the luminance values from morphTargets, and layer 4 filled
    % with 1's, so we can morph between the maskblob shape and a "constant one shape" simply by
    % drawing with the minimorphshader attached and modulateColor set to [w, 0, 0, 1-w] with w
    % moving between 0.0 and 1.0. We can morph between an "constant zero shape" and the morphTargets
    % shape by modulateColor set to [w, 0, 0, 0] with w moving between 0.0 and 1.0.
    % For this to work we need to use the minimorphshader during texture drawing. For convenience
    % we already attach the minimorphshader to the texture here, for later use:
    ysd = ms / 1.5;
    xsd = ysd;
    morphTargets = 100 * exp( -((x / xsd).^2) - ((y / ysd).^2) );
    morphTex = Screen('MakeTexture', w, morphTargets, [], [], 2, [], minimorphshader);
    mRect = []; %OffsetRect(Screen('Rect', morphTex), imgRect(RectLeft), imgRect(RectTop) + 600);

    blobtex = CreateProceduralGaussBlob(w, 300, 300, [], 1);

    ESCAPE = KbName('ESCAPE');
    SPACE = KbName('space');
    LeftArrow = KbName('LeftArrow');

    while 1
        % Query current mouse cursor position:
        [mx, my, buttons]=GetMouse;

        % Query keyboard:
        [pressed secs keycode] = KbCheck;
        if pressed
            KbReleaseWait;

            % ESC exits demo.
            if keycode(ESCAPE)
                break;
            end

            % Left Cursor key switches "drawing tool" for mask:
            if keycode(LeftArrow)
                brushtype = mod(brushtype + 1, 3);
            end

            % Space key switches display mode: mask, intermediate steps, final stim:
            if keycode(SPACE)
                mode = mod(mode + 1, 4);
            end
        end

        % --------------- Update / Draw into alpha "mixing" mask texture: -------------------

        % Compute position and size of destinationrect for gaussian brush
        % texture:
        dRect = CenterRectOnPoint(Screen('Rect', gaussbrush), mx, my);

        % Any buttons pressed for drawing/erasing?
        if any(buttons)
            % Yes! Draw into alpha mask image:

            % Which mouse button, if any?
            if any(buttons(2:end))
                % 2nd, 3rd, ... button: Erase by overpainting with zero alpha:
                cfactor = 0;
                Screen('Blendfunction', masktex, GL_ONE, GL_ZERO);
            else
                % First or none. Draw and accumulate with positive alpha:
                cfactor = 1;
                Screen('Blendfunction', masktex, GL_ONE, GL_ONE);
            end

            % Which drawing tool?
            switch brushtype
                case 0,
                  % Gaussian blob texture:
                  Screen('DrawTexture', masktex, gaussbrush, [], dRect, [], [], [], cfactor * [coverage coverage coverage coverage]);
                case 1,
                  % Oval:
                  Screen('FillOval', masktex, cfactor * [coverage coverage coverage coverage], CenterRectOnPoint([0 0 40 40], mx, my));
                case 2,
                  % Text can only be drawn, not erased, so do nothing in "erase mode":
                  if cfactor > 0
                      % Paint:
                      Screen('DrawText', masktex, 'Hello!', mx, my, [1 1 1 1]);
                  end
            end
        end

        % Some useless animations, just to show we can...
        Screen('Blendfunction', masktex, GL_ONE, GL_ZERO);

        Screen('FillRect', masktex, [0 0 0 0], cRect);
        startAngle = mod(startAngle + 2, 360);
        Screen('FillArc', masktex, [1 1 1 1], cRect, startAngle, 20);
        
        % Another useless animation - a procedural sine grating:
        amplitude = 0.5;
        freq = 5/360;
        angle = 0;
        Screen('DrawTexture', masktex, gratingtex, [], gRect, angle, [], [], [], [], [], [phase, freq, amplitude, 0]);
        
        % And another one - a mask morphing from all-zero to a gauss blob to all-one and back:
        if 0
            % From 0 -> Blob -> 1 -> Blob -> 0
            morphValue = sin(((phase / 10) - 90) / 360 * 2 * pi) + 1;
            if morphValue < 1
                weights = [morphValue, 0, 0, 0];
            else
                eweight = morphValue - 1;
                weights = [1 - eweight, 0, 0, eweight];
            end
            Screen('DrawTexture', masktex, morphTex, [], mRect, [], [], [], weights);
        else
            % From 0 -> Superblob -> 0
            morphValue = (sin(((phase / 10) - 90) / 360 * 2 * pi) + 1) / 2;
            weights = [morphValue, 0, 0, 0];
%            Screen('DrawTexture', masktex, morphTex, [], mRect, [], [], [], weights);
        end

        if 1
            % Use shader to draw blob with controllable amplitude and standard deviation:
            % Parameter vector [amplitude, stddev, aspect, 0]:
            morphValue = (sin(((phase / 10) - 90) / 360 * 2 * pi) + 1) / 2;
            Screen('DrawTexture', masktex, blobtex, [], [], [], [], [], [], [], kPsychDontDoRotation, [morphValue * 10, 100, 1.0, 0]);
            %Screen('DrawTexture', masktex, blobtex, [], [], [], [], [], [], [], kPsychDontDoRotation, [1, 200 * morphValue, 1.0, 0]);
        end
        
        phase = phase + 4;

        % --------------- Update actual stimulus, using the alpha "mixing" mask texture: -------------------

        % Step 1: Draw the alpha-mask into the backbuffer.
        if mode > 0
            % Actual use of masktex to define transition/mix:

            % First clear framebuffer to backgroundcolor, not using
            % alpha blending (== GL_ONE, GL_ZERO). Enable all channels
            % for writing [1 1 1 1], so everything gets cleared to good
            % starting values:
            Screen('BlendFunction', w, GL_ONE, GL_ZERO, [1 1 1 1]);
            Screen('FillRect', w, backgroundcolor);

            % Then keep alpha blending disabled and draw the mask
            % texture, but *only* into the alpha channel. Don't touch
            % the RGB color channels but use the channel mask via
            % [R G B A] = [0 0 0 1] to only enable the alpha-channel
            % for drawing into it. Use of modulateColor = [1 0 0 0] and
            % the minimorphshader causes the red channel to be copied into
            % the alpha channel. As red == luminance this means the grayscale
            % luminance value of masktext directly defines the final mask weights.
            Screen('BlendFunction', w, GL_ONE, GL_ZERO, [0 0 0 1]);
            Screen('DrawTexture', w, masktex, [], [], [], [], [], [1 0 0 0], minimorphshader);
        else
            % Visualize the alpha/mask masktex itself to explain
            % the concept - alpha values of 1 will show as white,
            % values of zero as black, intermediates as gray levels:
            Screen('BlendFunction', w, GL_ONE, GL_ZERO, [1 1 1 1]);
            Screen('DrawTexture', w, masktex, [], [], [], [], [], [1 0 0 0], minimorphshader);
        end

        % Step 2: Draw first image. It is only/increasingly drawn where
        % the alpha-value in the backbuffer is 1.0 or close, leaving
        % the foveated area (low or zero alpha values) alone:
        % This is done by weighting each color value of each pixel
        % with the corresponding alpha-value in the backbuffer
        % (GL_DST_ALPHA). Disable alpha channel writes via [1 1 1 0], so
        % alpha mask stays untouched and only RGB color channels are
        % affected:
        if mode == 1 || mode == 3
            Screen('BlendFunction', w, GL_DST_ALPHA, GL_ZERO, [1 1 1 0]);
            Screen('DrawTexture', w, firstImage);
        end

        % Step 3: Draw second image, but only/increasingly where the
        % alpha-value in the backbuffer is zero or low: This is
        % done by weighting each color value with one minus the
        % corresponding alpha-value in the backbuffer
        % (GL_ONE_MINUS_DST_ALPHA).
        if mode == 2 || mode == 3
            Screen('BlendFunction', w, GL_ONE_MINUS_DST_ALPHA, GL_ONE, [1 1 1 0]);
            Screen('DrawTexture', w, secondImage);
        end

        % Draw some text with explanation of the different steps:
        switch(mode)
            case 0,
                txt = 'Step3: Draw into alpha mask texture around mouse position:\nThis shows the alpha mask texture used for mixing of the images (white = 1.0 alpha weight, black = 0.0 alpha weight)';
            case 1,
                txt = 'Step4: Draw first texture, but weight each incoming source color pixel by the alpha value stored in the framebuffers alpha channel';
            case 2,
                txt = 'Step5: Draw second texture, but weight each incoming source color pixel by 1 minus the alpha value stored in the framebuffers alpha channel';
            case 3,
                txt = 'Perform alpha weighted compositing (all previous steps together):\n1. Draw alpha weight mask according to mouse position,\n2. Overdraw with alpha-weighted first texture,\n3. Overdraw with 1-alpha weighted second texture.';
        end
        txt = [txt '\nPress the SPACE key to continue to next step.\nPress Cursor left key to switch drawing tool.\nPress ESCAPE to exit demo.\n'];
        txt = [txt 'Press left mouse button to draw mask, other mouse button to erase mask'];
        DrawFormattedText(w, txt, 0, 40, [1 0 0], 60);

        % Show final result on screen. The 'Flip' also clears the drawing
        % surface back to black background color and a zero alpha value:
        Screen('Flip', w);
    end

    % Display full image a last time, just for fun...
    Screen('BlendFunction', w, GL_ONE, GL_ZERO);
    Screen('DrawTexture', w, secondImage);
    Screen('Flip', w);

    sca;
    fprintf('End of ImageMixingTutorial. Bye!\n\n');
    return;
catch
    %this "catch" section executes in case of an error in the "try" section
    %above.  Importantly, it closes the onscreen window if its open.
    sca;
    ShowCursor;
    Priority(0);
    psychrethrow(psychlasterror);
end %try..catch..
