• Jump To … +
    ./demo/canvas-001.js ./demo/canvas-002.js ./demo/canvas-003.js ./demo/canvas-004.js ./demo/canvas-005.js ./demo/canvas-006.js ./demo/canvas-007.js ./demo/canvas-008.js ./demo/canvas-009.js ./demo/canvas-010.js ./demo/canvas-011.js ./demo/canvas-012.js ./demo/canvas-013.js ./demo/canvas-014.js ./demo/canvas-015.js ./demo/canvas-015a.js ./demo/canvas-016.js ./demo/canvas-017.js ./demo/canvas-018.js ./demo/canvas-019.js ./demo/canvas-019a.js ./demo/canvas-020.js ./demo/canvas-021.js ./demo/canvas-022.js ./demo/canvas-023.js ./demo/canvas-024.js ./demo/canvas-025.js ./demo/canvas-026.js ./demo/canvas-027.js ./demo/canvas-028.js ./demo/canvas-029.js ./demo/canvas-030.js ./demo/canvas-031.js ./demo/canvas-032.js ./demo/canvas-033.js ./demo/canvas-034.js ./demo/canvas-035.js ./demo/canvas-036.js ./demo/canvas-037.js ./demo/canvas-038.js ./demo/canvas-039.js ./demo/canvas-040.js ./demo/canvas-041.js ./demo/canvas-042.js ./demo/canvas-043.js ./demo/canvas-044.js ./demo/canvas-045.js ./demo/canvas-046.js ./demo/canvas-047.js ./demo/canvas-048.js ./demo/canvas-049.js ./demo/canvas-050.js ./demo/canvas-051.js ./demo/canvas-052.js ./demo/canvas-053.js ./demo/canvas-054.js ./demo/canvas-055.js ./demo/canvas-056.js ./demo/canvas-057.js ./demo/canvas-058.js ./demo/canvas-059.js ./demo/canvas-060.js ./demo/canvas-061.js ./demo/canvas-062.js ./demo/canvas-063.js ./demo/canvas-064.js ./demo/core-001.js ./demo/delaunator-001.js ./demo/delaunator-002.js ./demo/dom-001.js ./demo/dom-002.js ./demo/dom-003.js ./demo/dom-004.js ./demo/dom-005.js ./demo/dom-006.js ./demo/dom-007.js ./demo/dom-008.js ./demo/dom-009.js ./demo/dom-010.js ./demo/dom-011.js ./demo/dom-012.js ./demo/dom-013.js ./demo/dom-015.js ./demo/dom-016.js ./demo/filters-001.js ./demo/filters-002.js ./demo/filters-002a.js ./demo/filters-003.js ./demo/filters-004.js ./demo/filters-005.js ./demo/filters-006.js ./demo/filters-007.js ./demo/filters-008.js ./demo/filters-009.js ./demo/filters-010.js ./demo/filters-011.js ./demo/filters-012.js ./demo/filters-013.js ./demo/filters-014.js ./demo/filters-015.js ./demo/filters-016.js ./demo/filters-017.js ./demo/filters-018.js ./demo/filters-019.js ./demo/filters-020.js ./demo/filters-021.js ./demo/filters-022.js ./demo/filters-023.js ./demo/filters-024.js ./demo/filters-025.js ./demo/filters-026.js ./demo/filters-027.js ./demo/filters-101.js ./demo/filters-102.js ./demo/filters-103.js ./demo/filters-104.js ./demo/filters-105.js ./demo/filters-501.js ./demo/filters-502.js ./demo/filters-503.js ./demo/filters-504.js ./demo/filters-505.js ./demo/mediapipe-001.js ./demo/mediapipe-002.js ./demo/mediapipe-003.js ./demo/modules-001.js ./demo/modules-002.js ./demo/modules-003.js ./demo/modules-004.js ./demo/modules-005.js ./demo/packets-001.js ./demo/packets-002.js ./demo/particles-001.js ./demo/particles-002.js ./demo/particles-003.js ./demo/particles-004.js ./demo/particles-005.js ./demo/particles-006.js ./demo/particles-007.js ./demo/particles-008.js ./demo/particles-009.js ./demo/particles-010.js ./demo/particles-011.js ./demo/particles-012.js ./demo/particles-013.js ./demo/particles-014.js ./demo/particles-015.js ./demo/particles-016.js ./demo/rapier-001.js ./demo/snippets-001.js ./demo/snippets-002.js ./demo/snippets-003.js ./demo/snippets-004.js ./demo/snippets-005.js ./demo/snippets-006.js ./demo/temp-000.js ./demo/temp-001.js ./demo/temp-001a.js ./demo/temp-002.js ./demo/temp-003.js ./demo/temp-004.js ./demo/temp-005.js ./demo/temp-006.js ./demo/temp-007.js ./demo/temp-008.js ./demo/temp-009.js ./demo/temp-010.js ./demo/temp-011.js ./demo/temp-012.js ./demo/temp-013.js ./demo/temp-014.js ./demo/temp-015.js ./demo/temp-016.js ./demo/temp-017.js ./demo/temp-018.js ./demo/temp-018a.js ./demo/temp-019.js ./demo/temp-020.js ./demo/temp-021.js ./demo/temp-022.js ./demo/temp-023.js ./demo/temp-024.js ./demo/temp-025.js ./demo/temp-026.js ./demo/temp-027.js ./demo/temp-028.js ./demo/temp-029.js ./demo/temp-030.js ./demo/temp-031.js ./demo/temp-032.js ./demo/temp-033.js ./demo/temp-034.js ./demo/temp-035.js ./demo/temp-036.js ./demo/temp-037.js ./demo/temp-100.js ./demo/temp-101.js ./demo/temp-102.js ./demo/temp-103.js ./demo/temp-104.js ./demo/temp-200.js ./demo/temp-201.js ./demo/temp-301.js ./demo/temp-inkscapeSvgFilters.js ./demo/temp-lottie.js ./demo/tensorflow-001.js ./demo/tensorflow-002.js ./demo/utilities.js
  • §

    Demo Canvas 057

    Animation observer; animations controlled by scroll position

  • §

    Run code

    import * as scrawl from '../source/scrawl.js';
  • §

    Scene setup

    const fc = scrawl.library.canvas['fixed-canvas'],
    
        rc1 = scrawl.library.canvas['responsive-canvas-1'],
        rc2 = scrawl.library.canvas['responsive-canvas-2'],
        rc3 = scrawl.library.canvas['responsive-canvas-3'],
    
        bc1 = scrawl.library.canvas['banner-canvas-1'],
        bc2 = scrawl.library.canvas['banner-canvas-2'];
    
    const canvasSet = {
        fit: 'cover',
        checkForResize: true,
        ignoreCanvasCssDimensions: true,
        baseMatchesCanvasDimensions: true,
    };
    
    fc.set(canvasSet);
    
    rc1.set(canvasSet);
    rc2.set(canvasSet);
    rc3.set(canvasSet);
    
    bc1.set(canvasSet);
    bc2.set(canvasSet);
  • §

    Fixed canvas

    Update the text values of three Phrase entitys based on the positions of the top, center and base of the canvas element in the viewport

    let fixedCanvasIsDisplaying,
        fixedCanvasDragZone;
    
    const fcAnimation = scrawl.makeRender({
    
        name: 'fc-animation',
        target: fc,
    });
  • §

    Responsive canvas 1

    Update the text values of three Phrase entitys based on the positions of the top, center and base of the <canvas> element in the viewport. The element is responsive, with positions updating as the element is resized.

    const rc1PhraseTop = scrawl.makePhrase({
    
        name: 'rc1-pos-top',
        group: rc1.base.name,
        start: ['center', '5%'],
        handle: ['center', 'center'],
        font: '20px Arial, sans-serif',
        lineHeight: 1,
    });
    
    const rc1PhraseCenter = rc1PhraseTop.clone({
    
        name: 'rc1-pos-center',
        startY: 'center',
    });
    
    const rc1PhraseBase = rc1PhraseTop.clone({
    
        name: 'rc1-pos-base',
        startY: '95%',
    });
    
    const updateRc1 = () => {
    
        const {here} = rc1;
        const { inViewportTop, inViewportCenter, inViewportBase } = here;
    
        rc1PhraseTop.set({ text: `Canvas top: ${(here.inViewportTop * 100).toFixed(2)}%` });
        rc1PhraseCenter.set({ text: `Canvas center: ${(here.inViewportCenter * 100).toFixed(2)}%` });
        rc1PhraseBase.set({ text: `Canvas base: ${(here.inViewportBase * 100).toFixed(2)}%` });
    };
    
    const rc1Animation = scrawl.makeRender({
    
        name: "rc1-animation",
        target: rc1,
        observer: true,
        commence: updateRc1,
    });
  • §

    Responsive canvas 2

    Update the start and end points of a gradient based on the positions of the top and base of the <canvas> element in the viewport. Link artefact animation to the element’s position.

    const rc2Color1 = scrawl.makeColor({
    
        name: 'rc2-color-factory-1',
        maximumColor: 'red',
        minimumColor: 'blue',
    });
    
    const rc2Color2 = scrawl.makeColor({
    
        name: 'rc2-color-factory-2',
        maximumColor: 'green',
        minimumColor: 'yellow',
    });
    
    const rc2Gradient = scrawl.makeGradient({
    
        name: 'rc2-gradient',
        endX: '100%',
    });
    
    scrawl.makeLine({
    
        name: 'rc2-line',
        group: rc2.base.name,
        start: ['center', 'center'],
        end: ['90%', 'center'],
        handleX: '50%',
        method: 'draw',
        strokeStyle: 'rc2-gradient',
        lockStrokeStyleToEntity: true,
        lineWidth: 5,
        lineCap: 'round',
    });
    
    scrawl.makeBlock({
    
        name: 'rc2-block-1',
        group: rc2.base.name,
        start: ['25%', '25%'],
        handle: ['center', 'center'],
        dimensions: ['40%', '10%'],
        method: 'fillThenDraw',
        fillStyle: 'rc2-gradient',
        lockFillStyleToEntity: true,
        lineWidth: 5,
        lineCap: 'round',
    
    }).clone({
    
        name: 'rc2-block-2',
        start: ['75%', '75%'],
        flipReverse: true,
    });
    
    scrawl.makeWheel({
    
        name: 'rc2-wheel-1',
        group: rc2.base.name,
        start: ['25%', '75%'],
        handle: ['center', 'center'],
        radius: '20%',
        startAngle: 20,
        endAngle: -20,
        includeCenter: true,
        method: 'fillThenDraw',
        fillStyle: 'rc2-gradient',
        lockFillStyleToEntity: true,
        lineWidth: 5,
        lineCap: 'round',
    
    }).clone({
    
        name: 'rc2-wheel-2',
        start: ['75%', '25%'],
        flipReverse: true,
    });
    
    const updateRc2 = () => {
    
        const {here} = rc2;
        let { inViewportTop, inViewportCenter, inViewportBase } = here;
    
        const group = scrawl.library.group[rc2.base.name];
    
        if (inViewportTop < 0) inViewportTop = 0;
        else if (inViewportTop > 1) inViewportTop = 1;
    
        if (inViewportBase < 0) inViewportBase = 0;
        else if (inViewportBase > 1) inViewportBase = 1;
    
        if (inViewportCenter < 0) inViewportCenter = 0;
        else if (inViewportCenter > 1) inViewportCenter = 1;
    
        group.setArtefacts({ roll: inViewportCenter * -360 });
    
        rc2Gradient.updateColor(0, rc2Color1.getRangeColor(inViewportTop));
        rc2Gradient.updateColor(999, rc2Color2.getRangeColor(inViewportBase));
    };
    
    const rc2Animation = scrawl.makeRender({
    
        name: "rc2-animation",
        target: rc2,
        observer: true,
        commence: updateRc2,
    });
  • §

    Banner canvas 1

    Fix an animation to the viewport while the user scrolls through the page. When the animation completes, the <canvas> element rejoins the page scroll. The canvas remains interactive before, during and after the animation’s run. The animation is reversible.

    const bc1Cell = bc1.buildCell({
    
        name: 'bc1-visual-cell',
        width: '100%',
        height: `${100 / 3}%`,
        backgroundColor: 'darkslategray',
    });
    
    const bc1BalancePoint = 2500;
    
    const bc1Arrow = scrawl.makeShape({
    
        name: 'bc1-arrow',
        group: 'bc1-visual-cell',
    
        pathDefinition: 'M266.2,703.1 h-178 L375.1,990 l287-286.9 H481.9 C507.4,365,683.4,91.9,911.8,25.5 877,15.4,840.9,10,803.9,10 525.1,10,295.5,313.4,266.2,703.1 z',
    
        start: ['center', 'center'],
        handle: ['center', 'center'],
    
        strokeStyle: 'red',
        fillStyle: 'lavender',
        lineWidth: 20,
        lineJoin: 'round',
        lineCap: 'round',
    
        scale: 0.7,
        scaleOutline: false,
        roll: -90,
  • §

    We create this effect using a dashed line with very large dash/nodash values

    • We can then set the offset to the point where the displayed dash ends, so it looks like the arrow doesn’t have a stroke
        lineDash: [bc1BalancePoint, bc1BalancePoint],
        lineDashOffset: bc1BalancePoint,
  • §

    To retrieve the Shape’s length, we need to tell it that it is being used as a path

        useAsPath: true,
        precision: 1,
    
        method: 'fillThenDraw',
    });
    
    const bc1ArrowProgress = scrawl.makeWorld({
    
        name: 'bc1-world',
    
        userAttributes: [
    
            {
                key: 'progress', 
                defaultValue: 0,
                setter: function (item) {
    
                    this.progress = item;
    
                    if (bc1Arrow.length != null) {
    
                        bc1Arrow.set({ lineDashOffset: bc1BalancePoint - Math.round(bc1Arrow.length * item) });
                    }
                },
            }
        ],
    });
    
    const bc1Phrase = scrawl.makePhrase({
    
        name: 'bc1-phrase',
        group: 'bc1-visual-cell',
        start: ['65%', '63%'],
        handle: ['center', 'center'],
        font: '20px Arial, sans-serif',
        lineHeight: 1,
    });
    
    scrawl.makeGroup({
    
        name: 'bc1-drag-group',
        host: 'bc1-visual-cell',
        order: 0,
    });
    
    scrawl.makeBlock({
    
        name: 'bc1-drag-box-1',
        group: 'bc1-drag-group',
        dimensions: [150, 70],
        start: ['10%', '25%'],
        handle: ['center', 'center'],
        fillStyle: 'white',
        lineWidth: 4,
        method: 'fillThenDraw',
    
        onEnter: function () {
    
            bc1.set({ css: { cursor: 'pointer' }});
            fc.set({ css: { cursor: 'pointer' }});
  • §

    @ts-expect-error

            this.set({ 
                fillStyle: 'pink',
                lineWidth: 8,
            });
        },
    
        onLeave: function () {
    
            bc1.set({ css: { cursor: 'auto' }});
            fc.set({ css: { cursor: 'auto' }});
  • §

    @ts-expect-error

            this.set({ 
                fillStyle: 'white',
                lineWidth: 4,
            });
        },
    
    }).clone({
    
        name: 'bc1-drag-box-2',
        startY: '50%',
    
    }).clone({
    
        name: 'bc1-drag-box-3',
        startY: '75%',
    });
    
    scrawl.makeGroup({
    
        name: 'bc1-label-group',
        host: 'bc1-visual-cell',
        order: 1,
    });
    
    scrawl.makePhrase({
    
        name: 'bc1-drag-box-label-1',
        group: 'bc1-label-group',
        font: '20px Arial, sans-serif',
        text: 'DRAG ME!',
        pivot: 'bc1-drag-box-1',
        lockTo: 'pivot',
        handle: ['center', 'center'],
        lineHeight: 0.7,
    
    }).clone ({
    
        name: 'bc1-drag-box-label-2',
        pivot: 'bc1-drag-box-2',
    
    }).clone ({
    
        name: 'bc1-drag-box-label-3',
        pivot: 'bc1-drag-box-3',
    });
    
    scrawl.makeDragZone({
    
        zone: bc1,
        collisionGroup: 'bc1-drag-group',
        coordinateSource: bc1Cell,
        endOn: ['up', 'leave'],
        preventTouchDefaultWhenDragging: true,
    });
    
    const bc1Display = scrawl.makePicture({
    
        name: 'bc1-display',
        group: fc.base.name,
        asset: 'bc1-visual-cell',
        dimensions: ['100%', '100%'],
        copyDimensions: ['100%', '100%'],
        visibility: false,
    });
    
    scrawl.addListener('move', () => bc1.cascadeEventAction('move'), bc1.domElement);
    
    const updateBc1 = () => {
    
        bc1Cell.updateHere();
    
        const { here } = bc1;
        const { inViewportTop } = here;
    
        bc1Phrase.set({ text: `Canvas top position: ${inViewportTop.toFixed(4)}` });
    
        if (inViewportTop > 0 || inViewportTop < -2) {
    
            fc.domElement.style.transform = 'translateY(200%)';
    
            if (fixedCanvasIsDisplaying) {
    
                fixedCanvasIsDisplaying();
                fixedCanvasIsDisplaying = false;
    
                if (fixedCanvasDragZone) fixedCanvasDragZone();
                fixedCanvasDragZone = false;
    
                bc1Cell.set({ startY: (inViewportTop > 0) ? '0%' : `${ 200 / 3 }%` });
            };
        }
        else {
    
            fc.domElement.style.transform = 'translateY(0%)';
    
            if (!fixedCanvasIsDisplaying) {
    
                fixedCanvasIsDisplaying = scrawl.addListener('move', () => bc1.cascadeEventAction('move'), fc.domElement);
    
                fixedCanvasDragZone = scrawl.makeDragZone({
    
                    zone: fc,
                    collisionGroup: 'bc1-drag-group',
                    endOn: ['up', 'leave'],
                });
            };
  • §

    @ts-expect-error

           bc1ArrowProgress.set({ progress: inViewportTop * (-1 / 2) });
    
            bc1Cell.set({ startY: `${ (inViewportTop * -100) / 3 }%` });
        }
    };
    
    const bc1Animation = scrawl.makeRender({
    
        name: "bc1-animation",
        target: bc1,
        observer: true,
        commence: updateBc1,
    
        onRun: () => bc1Display.set({ visibility: true }),
        onHalt: () => bc1Display.set({ visibility: false }),
    });
  • §

    Responsive canvas 3

    Link progression of tween-based animations to the position of the center of the <canvas> element in the viewport. Trigger timeline actions when the center of the element passes the action’s breakpoint. Tweens and actions are reversible.

    rc3.set({ backgroundColor: 'red' });
    
    scrawl.makeGradient({
    
        name: 'rc3-gradient',
        end: ['100%', '100%'],
    });
    
    scrawl.makeLine({
    
        name: 'rc3-line',
        group: rc3.base.name,
        start: ['center', 'center'],
        end: ['center', '10%'],
        handleY: '-50%',
        method: 'draw',
        lineWidth: 5,
        lineCap: 'round',
    });
    
    scrawl.makeBlock({
    
        name: 'rc3-block-1',
        group: rc3.base.name,
        start: ['25%', '25%'],
        handle: ['center', 'center'],
        dimensions: ['40%', '10%'],
        method: 'fillThenDraw',
        fillStyle: 'rc3-gradient',
        lockFillStyleToEntity: true,
        lineWidth: 5,
        lineCap: 'round',
    
    }).clone({
    
        name: 'rc3-block-2',
        start: ['75%', '75%'],
        flipReverse: true,
    });
    
    scrawl.makeWheel({
    
        name: 'rc3-wheel-1',
        group: rc3.base.name,
        start: ['25%', '75%'],
        handle: ['center', 'center'],
        radius: '20%',
        startAngle: 20,
        endAngle: -20,
        includeCenter: true,
        method: 'fillThenDraw',
        fillStyle: 'rc3-gradient',
        lockFillStyleToEntity: true,
        lineWidth: 5,
        lineCap: 'round',
    
    }).clone({
    
        name: 'rc3-wheel-2',
        start: ['75%', '25%'],
        flipReverse: true,
    });
    
    const rc3Ticker = scrawl.makeTicker({
        name: 'rc3-ticker',
        cycles: 0,
        duration: 1000,
    });
    
    scrawl.makeTween({
        name: 'rc3-block-tween',
        targets: ['rc3-block-1', 'rc3-block-2'],
        ticker: 'rc3-ticker',
        duration: 600,
        time: 0,
        definitions: [
            {
                attribute: 'roll',
                start: 0,
                end: 720,
                engine: 'easeOut',
            },
        ],
    });
    
    scrawl.makeTween({
    
        name: 'rc3-wheel-tween',
        targets: ['rc3-wheel-1', 'rc3-wheel-2'],
        ticker: 'rc3-ticker',
        duration: 600,
        time: 400,
        definitions: [
            {
                attribute: 'roll',
                start: 0,
                end: -720,
                engine: 'easeOut',
            },
        ],
    });
    
    scrawl.makeTween({
    
        name: 'rc3-line-tween',
        targets: 'rc3-line',
        ticker: 'rc3-ticker',
        duration: 800,
        time: 100,
        definitions: [
            {
                attribute: 'lineWidth',
                start: 6,
                end: 30,
            },
        ],
    });
    
    scrawl.makeAction({
    
        name: 'rc3-action-250',
        ticker: 'rc3-ticker',
        time: 250,
        action: () => rc3.set({ backgroundColor: 'blue'}),
        revert: () => rc3.set({ backgroundColor: 'red'}),
    });
    
    scrawl.makeAction({
    
        name: 'rc3-action-500',
        ticker: 'rc3-ticker',
        time: 500,
        action: () => rc3.set({ backgroundColor: 'green'}),
        revert: () => rc3.set({ backgroundColor: 'blue'}),
    });
    
    scrawl.makeAction({
    
        name: 'rc3-action-750',
        ticker: 'rc3-ticker',
        time: 750,
        action: () => rc3.set({ backgroundColor: 'yellow'}),
        revert: () => rc3.set({ backgroundColor: 'green'}),
    });
    
    const updateRc3 = () => {
    
        const {here} = rc3;
        let { inViewportCenter } = here;
    
        if (inViewportCenter < 0) inViewportCenter = 0;
        else if (inViewportCenter > 1) inViewportCenter = 1;
    
        rc3Ticker.seekTo(inViewportCenter * 1000);
    };
    
    const rc3Animation = scrawl.makeRender({
    
        name: "rc3-animation",
        target: rc3,
        observer: true,
        commence: updateRc3,
    });
  • §

    Banner canvas 2

    Play a video (after user interaction) only while the <canvas> element is fully visible in the viewport. Show scroll progression using a graphic dial and text. The blurred background moves upwards (parallax) as the user scrolls down. The parallax effect and dial/text animation are reversible.

    const bc2Cell = bc2.buildCell({
    
        name: 'bc2-visual-cell',
        width: '100%',
        height: `${100 / 4}%`,
        backgroundColor: 'green',
    });
    
    scrawl.makeFilter({
    
        name: 'bc2-gaussian-blur',
        method: 'gaussianBlur',
        radius: 8,
    });
    
    const bc2Video1 = scrawl.makePicture({
    
        name: 'bc2-video-1',
        group: 'bc2-visual-cell',
    
        videoSource: 'img/swans.mp4',
    
        width: '100%',
        height: '100%',
    
        copyWidth: '50%',
        copyHeight: '50%',
    
        copyStart: ['25%', '25%'],
    
        filters: ['bc2-gaussian-blur'],
    });
    
    const bc2Video2 = bc2Video1.clone({
    
        name: 'bc2-video-2',
        start: ['center', '60%'],
        handle: ['center', 'top'],
        dimensions: [320, 180],
        filters: [],
        strokeStyle: 'red',
        lineWidth: 2,
        method: 'fillThenDraw',
    });
    
    let bc2UsertInteractionConfirmed = false;
    const bc2InitialVideoStart = scrawl.addListener('up', () => {
    
        bc2Video2.set({ 
            video_loop: true, 
        });
    
        bc2UsertInteractionConfirmed = true;
  • §

    Get rid of the event listener after invocation - it’s a one-time-only action

        bc2InitialVideoStart();
    
    }, [bc2.domElement, fc.domElement]);
    
    const bc2Phrase = scrawl.makePhrase({
    
        name: 'bc2-phrase',
        group: 'bc2-visual-cell',
        start: ['center', 'center'],
        handle: ['center', 'center'],
        font: '20px Arial, sans-serif',
        lineHeight: 1,
    });
    
    const bc2DialWheel = scrawl.makeWheel({
    
        name: 'bc2-dial-wheel',
        group: 'bc2-visual-cell',
        start: ['25%', '25%'],
        handle: ['center', 'center'],
        radius: '15%',
        lineWidth: 8,
        includeCenter: true,
        lineJoin: 'round',
        lineCap: 'round',
        flipReverse: true,
        fillStyle: 'green',
        strokeStyle: 'white',
        method: 'fillThenDraw',
    });
    
    bc2DialWheel.clone({
        
        name: 'bc2-dial-wheel-pin',
        pivot: 'bc2-dial-wheel',
        lockTo: 'pivot',
        fillStyle: 'white',
        includeCenter: false,
        scale: 0.2,
        method: 'fill',
    });
    
    bc2DialWheel.clone({
    
        name: 'bc2-dial-wheel-bottom',
        pivot: 'bc2-dial-wheel',
        lockTo: 'pivot',
        fillStyle: 'white',
        includeCenter: false,
        flipReverse: false,
        startAngle: 0,
        endAngle: 180,
        method: 'fill',
    });
    
    const bc2DialProgress = scrawl.makePhrase({
    
        name: 'bc2-dial-wheel',
        group: 'bc2-visual-cell',
        font: '40px Arial, sans-serif',
        pivot: 'bc2-dial-wheel',
        lockTo: 'pivot',
        handle: ['center', '-40%'],
        text: '0%',
    });
    
    const bc2Color = scrawl.makeColor({
    
        name: 'bc2-color-factory',
        maximumColor: 'red',
        minimumColor: 'green',
        easing: 'easeOutCubic',
    });
    
    const bc2Display = scrawl.makePicture({
    
        name: 'bc2-display',
        group: fc.base.name,
        asset: 'bc2-visual-cell',
        dimensions: ['100%', '100%'],
        copyDimensions: ['100%', '100%'],
        visibility: false,
    });
    
    const updateBc2 = () => {
    
        const { here } = bc2;
        const { inViewportTop } = here;
    
        if (inViewportTop > 0 || inViewportTop < -3) {
  • §

    fc.domElement.style.transform = translateY(${inViewportTop * 100}%);

            fc.domElement.style.transform = 'translateY(200%)';
    
            if (fixedCanvasIsDisplaying) {
    
                fixedCanvasIsDisplaying();
                fixedCanvasIsDisplaying = false;
    
                if (fixedCanvasDragZone) fixedCanvasDragZone();
                fixedCanvasDragZone = false;
    
                bc2Cell.set({ 
                    startY: (inViewportTop > 0) ? '0%' : '75%',
                    shown: true, 
                });
            };
        }
        else {
    
            fc.domElement.style.transform = 'translateY(0%)';
    
            if (!fixedCanvasIsDisplaying) {
    
                fixedCanvasIsDisplaying = () => {};
                fixedCanvasDragZone = () => {};
    
                bc2Cell.set({ shown: false });
            };
    
            bc2DialWheel.set({ 
    
                fillStyle: bc2Color.getRangeColor(-inViewportTop / 3),
                roll: (-inViewportTop / 3) * 180,
            });
    
            bc2DialProgress.set({ text: `${((-inViewportTop / 3) * 100).toFixed(0)}%`});
    
            bc2Video1.set({ copyStartY: `${25 + (inViewportTop * -10)}%`});
        }
    
        if (bc2UsertInteractionConfirmed) {
    
            const currentVideoTime = bc2Video2.get('video_currentTime');
    
            if (inViewportTop <= 0 && inViewportTop >= -3) {
    
                if (bc2Video2.get('video_paused')) bc2Video2.videoPlay();
    
                bc2Phrase.set({ 
                    text: `Video is currently playing - ${currentVideoTime.toFixed(1)}s`,
                });
            }
            else {
    
                if (!bc2Video2.get('video_paused')) bc2Video2.videoPause();
    
                bc2Phrase.set({ 
                    text: `Video is currently paused at ${currentVideoTime.toFixed(1)}s`,
                });
            }
        }
        else bc2Phrase.set({ text: 'Click to play video' });
    };
    
    const bc2Animation = scrawl.makeRender({
    
        name: "bc2-animation",
        target: bc2,
        observer: true,
        commence: updateBc2,
    
        onRun: () => bc2Display.set({ visibility: true }),
        onHalt: () => bc2Display.set({ visibility: false }),
    });
  • §

    Scene animation reporter

    Function to display whether each canvas wrapper’s associated animation object is currently running

    const report = document.querySelector('#report');
    
    scrawl.makeAnimation({
    
        name: 'reporter',
        fn: () => {
            report.textContent = `Running animations - RC1: ${rc1Animation.isRunning()}, RC2: ${rc2Animation.isRunning()}, BC1: ${bc1Animation.isRunning()}, RC3: ${rc3Animation.isRunning()}, BC2: ${bc2Animation.isRunning()}`;
        },
    });
    
    
    console.log(scrawl.library);