REPOST dr TETANGGA
ANIMASI GARIS
CREATED by MARVELOUSLY HELP ME of DEEP SEEK
*atur screen zoom-in dan zoom-out utk TEPAT Readability

Click here for My DeepSeek Session of building this App


Aliran Banyak Panah multi‑arah + kedip

150 px

 Ruas garis & sudut belok

⚡ aliran
✨ Bobot total: 0 (dinormalkan ke 100%)
panah menghadap arah gerak · berkedip · jumlah panah dapat diatur


S O U R C E  C O D E

<style>
    #apw-wrap {
        font-family: "Segoe UI", Roboto, Arial, sans-serif;
        background: #eef2f5;
        padding: 12px;
        display: flex;
        justify-content: center;
        align-items: flex-start;
    }
    #apw-wrap .apw-container {
        background: white;
        border-radius: 28px;
        box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
        padding: 28px;
        max-width: 820px;
        width: 100%;
    }
    #apw-wrap h2 {
        margin: 0 0 12px 0;
        color: #1e2b3a;
        font-weight: 500;
        display: flex;
        align-items: center;
        gap: 8px;
        border-bottom: 2px solid #cfdee9;
        padding-bottom: 16px;
    }
    #apw-wrap h2 span {
        background: #0066cc;
        color: white;
        font-size: 0.9rem;
        padding: 4px 14px;
        border-radius: 40px;
        font-weight: 400;
    }
    #apw-wrap canvas {
        display: block;
        margin: 0 auto 22px auto;
        border: 2px solid #a0c0d4;
        border-radius: 20px;
        background: #fafeff;
        width: 100%;
        height: auto;
        cursor: crosshair;
        box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.02);
    }
    #apw-wrap .apw-control-panel {
        display: flex;
        flex-direction: column;
        gap: 24px;
    }
    #apw-wrap .apw-slider-group {
        display: flex;
        flex-direction: column;
        gap: 8px;
    }
    #apw-wrap .apw-slider-group label {
        font-weight: 600;
        color: #144a6f;
        font-size: 1rem;
        letter-spacing: 0.3px;
    }
    #apw-wrap .apw-slider-row {
        display: flex;
        align-items: center;
        gap: 16px;
    }
    #apw-wrap .apw-slider-row input[type="range"] {
        flex: 1;
        height: 7px;
        border-radius: 20px;
        background: #d3e2ed;
    }
    #apw-wrap .apw-slider-row span {
        min-width: 60px;
        text-align: right;
        font-weight: 700;
        color: #0b4770;
    }
    #apw-wrap .apw-segmen-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin: 6px 0 10px 0;
    }
    #apw-wrap .apw-segmen-header h3 {
        margin: 0;
        font-size: 1.15rem;
        font-weight: 500;
        color: #1d3e5e;
    }
    #apw-wrap .apw-btn-tambah {
        background: #28a745;
        color: white;
        border: none;
        padding: 8px 20px;
        border-radius: 40px;
        font-weight: 600;
        font-size: 0.9rem;
        cursor: pointer;
        transition: 0.15s;
        box-shadow: 0 3px 8px rgba(40, 167, 69, 0.25);
    }
    #apw-wrap .apw-btn-tambah:hover {
        background: #218838;
        transform: scale(1.02);
    }
    #apw-wrap .apw-btn-hapus {
        background: #dc3545;
        color: white;
        border: none;
        padding: 8px 20px;
        border-radius: 40px;
        font-weight: 600;
        font-size: 0.9rem;
        cursor: pointer;
        transition: 0.15s;
        box-shadow: 0 3px 8px rgba(220, 53, 69, 0.25);
    }
    #apw-wrap .apw-btn-hapus:hover {
        background: #c82333;
        transform: scale(1.02);
    }
    #apw-wrap .apw-btn-hapus:disabled {
        background: #b3b3b3;
        pointer-events: none;
        opacity: 0.5;
    }
    #apw-segmenList {
        background: #f2f8ff;
        border-radius: 24px;
        padding: 18px 14px;
        border: 1px solid #cbdbe9;
        display: flex;
        flex-direction: column;
        gap: 20px;
    }
    #apw-wrap .apw-segmen-item {
        background: white;
        border-radius: 20px;
        padding: 16px 18px;
        box-shadow: 0 3px 8px rgba(0, 40, 70, 0.08);
        border-left: 6px solid #4b8fc8;
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 16px;
    }
    #apw-wrap .apw-segmen-index {
        font-weight: 700;
        background: #e1effb;
        color: #004c99;
        padding: 6px 14px;
        border-radius: 30px;
        font-size: 0.9rem;
    }
    #apw-wrap .apw-bobot-control {
        display: flex;
        align-items: center;
        gap: 10px;
        flex: 2 1 200px;
    }
    #apw-wrap .apw-bobot-control label {
        font-weight: 600;
        color: #2b4d6a;
        min-width: 55px;
    }
    #apw-wrap .apw-bobot-control input[type="number"] {
        width: 85px;
        padding: 8px;
        border: 2px solid #bcd4e6;
        border-radius: 26px;
        text-align: center;
        font-weight: 600;
        color: #003366;
        background: #f5faff;
        transition: 0.15s;
    }
    #apw-wrap .apw-bobot-control input[type="number"]:focus {
        border-color: #0077be;
        outline: none;
        background: white;
    }
    #apw-wrap .apw-sudut-control {
        display: flex;
        align-items: center;
        gap: 12px;
        flex: 3 1 260px;
    }
    #apw-wrap .apw-sudut-control label {
        font-weight: 600;
        color: #2b4d6a;
        min-width: 40px;
    }
    #apw-wrap .apw-sudut-control input[type="range"] {
        flex: 1;
        min-width: 140px;
    }
    #apw-wrap .apw-sudut-control span {
        min-width: 45px;
        text-align: right;
        font-weight: 700;
        color: #004182;
    }
    #apw-wrap .apw-radio-group {
        display: flex;
        gap: 30px;
        align-items: center;
        flex-wrap: wrap;
        background: #e9f0f7;
        padding: 15px 20px;
        border-radius: 60px;
    }
    #apw-wrap .apw-radio-group label {
        font-weight: 550;
        color: #1e3b58;
        display: flex;
        align-items: center;
        gap: 9px;
        cursor: pointer;
    }
    #apw-wrap .apw-radio-group input[type="radio"] {
        accent-color: #0066cc;
        width: 18px;
        height: 18px;
    }
    #apw-wrap .apw-panah-control {
        background: #e9f0f7;
        border-radius: 60px;
        padding: 12px 24px;
        display: flex;
        align-items: center;
        gap: 24px;
        flex-wrap: wrap;
    }
    #apw-wrap .apw-panah-control label {
        font-weight: 600;
        color: #144a6f;
        display: flex;
        align-items: center;
        gap: 10px;
    }
    #apw-wrap .apw-panah-control input[type="range"] {
        width: 200px;
    }
    #apw-wrap .apw-panah-control span {
        font-weight: 700;
        color: #0066cc;
        min-width: 30px;
        text-align: center;
    }
    #apw-wrap .apw-button-bar {
        display: flex;
        gap: 12px;
        flex-wrap: wrap;
        justify-content: center;
        margin-top: 8px;
    }
    #apw-wrap .apw-button-bar button {
        background: #0066cc;
        border: 1px solid rgba(255, 255, 255, 0.25);
        color: white;
        padding: 12px 26px;
        border-radius: 60px;
        font-size: 1rem;
        font-weight: 600;
        cursor: pointer;
        transition: 0.2s;
        box-shadow: 0 5px 12px rgba(0, 102, 204, 0.35);
        flex: 1 0 140px;
    }
    #apw-wrap .apw-button-bar button:hover {
        background: #004c99;
        transform: translateY(-3px);
        box-shadow: 0 9px 18px rgba(0, 102, 204, 0.45);
    }
    #apw-wrap .apw-button-bar button:active {
        transform: translateY(1px);
    }
    #apw-wrap #apwStopBtn {
        background: #6c757d;
        box-shadow: 0 5px 10px rgba(108, 117, 125, 0.3);
    }
    #apw-wrap #apwStopBtn:hover {
        background: #5a6268;
    }
    #apw-wrap #apwResetBtn {
        background: #28a745;
        box-shadow: 0 5px 10px rgba(40, 167, 69, 0.3);
    }
    #apw-wrap #apwResetBtn:hover {
        background: #218838;
    }
    #apw-wrap .apw-info-panel {
        background: #e2eaf2;
        border-radius: 60px;
        padding: 12px 22px;
        text-align: center;
        color: #113750;
        font-size: 0.95rem;
        border: 1px solid #b8cfe4;
        display: flex;
        justify-content: space-between;
        flex-wrap: wrap;
    }
    #apw-wrap .apw-total-bobot {
        background: #004a7c;
        color: white;
        border-radius: 40px;
        padding: 4px 16px;
    }
    #apw-wrap footer {
        text-align: center;
        font-size: 0.8rem;
        color: #6a7e94;
        margin-top: 24px;
    }
</style>

<div id="apw-wrap">
    <div class="apw-container">
        <h2> Aliran Banyak Panah <span>multi‑arah + kedip</span></h2>

        <canvas height="360" id="apwCanvas" width="640"></canvas>

        <div class="apw-control-panel">
            <div class="apw-slider-group">
                <label> Panjang total garis (piksel)</label>
                <div class="apw-slider-row">
                    <input id="apwPanjangSlider" max="280" type="range" value="150" />
                    <span id="apwPanjangDisplay">150 px</span>
                </div>
            </div>

            <div class="apw-segmen-header">
                <h3> Ruas garis &amp; sudut belok</h3>
                <div style="display: flex; gap: 8px">
                    <button class="apw-btn-tambah" id="apwTambahSegmenBtn">➕ Tambah ruas</button>
                    <button class="apw-btn-hapus" disabled="disabled" id="apwHapusSegmenBtn">✖ Hapus terakhir</button>
                </div>
            </div>

            <div id="apw-segmenList"></div>

            <div class="apw-radio-group">
                <label
                    ><input checked="checked" id="apwForwardRadio" name="apwArahPanah" type="radio" value="forward" />
                    ▶ Forward (awal → akhir)</label
                >
                <label
                    ><input id="apwBackwardRadio" name="apwArahPanah" type="radio" value="backward" /> ◀ Backward
                    (akhir → awal)</label
                >
            </div>

            <div class="apw-panah-control">
                <label> Jumlah panah <span id="apwJmlPanahValue">3</span></label>
                <input id="apwJmlPanahSlider" max="10" type="range" value="3" />
                <span>⚡ aliran</span>
            </div>

            <div class="apw-button-bar">
                <button id="apwStartBtn">▶ Mulai Aliran</button>
                <button id="apwStopBtn">⏸ Berhenti</button>
                <button id="apwResetBtn">↻ Reset ke Awal</button>
            </div>

            <div class="apw-info-panel">
                <span>✨ Bobot total: <span id="apwTotalBobotSpan">0</span></span>
                <span class="apw-total-bobot" id="apwBobotNormalInfo">(dinormalkan ke 100%)</span>
            </div>
        </div>

        <footer>panah menghadap arah gerak · berkedip · jumlah panah dapat diatur</footer>
    </div>
</div>

<script>
    (function () {
        var canvas = document.getElementById("apwCanvas");
        var ctx = canvas.getContext("2d");
        var panjangSlider = document.getElementById("apwPanjangSlider");
        var panjangDisplay = document.getElementById("apwPanjangDisplay");
        var forwardRadio = document.getElementById("apwForwardRadio");
        var backwardRadio = document.getElementById("apwBackwardRadio");
        var jmlPanahSlider = document.getElementById("apwJmlPanahSlider");
        var jmlPanahValue = document.getElementById("apwJmlPanahValue");
        var startBtn = document.getElementById("apwStartBtn");
        var stopBtn = document.getElementById("apwStopBtn");
        var resetBtn = document.getElementById("apwResetBtn");
        var tambahSegmen = document.getElementById("apwTambahSegmenBtn");
        var hapusSegmen = document.getElementById("apwHapusSegmenBtn");
        var segmenListDiv = document.getElementById("apw-segmenList");
        var totalBobotSpan = document.getElementById("apwTotalBobotSpan");
        var panjangTotal = 150;
        var arahPanah = 1;
        var progressUtama = 0.0;
        var jumlahPanah = 3;
        var animActive = false;
        var animId = null;
        var SPEED = 0.004;
        var segmen = [
            { bobot: 50, sudut: 0 },
            { bobot: 30, sudut: 45 },
            { bobot: 20, sudut: -30 },
        ];
        var MAX_SEGMEN = 8;

        function renderSegmenList() {
            var html = "";
            for (var i = 0; i < segmen.length; i++) {
                var s = segmen[i];
                var isFirst = i === 0;
                html += '<div class="apw-segmen-item" data-index="' + i + '">';
                html += '<div class="apw-segmen-index">Ruas ' + (i + 1) + "</div>";
                html += '<div class="apw-bobot-control"><label>Bobot</label>';
                html +=
                    '<input type="number" min="1" max="500" step="1" value="' +
                    s.bobot +
                    '" class="apw-bobot-input" data-index="' +
                    i +
                    '"></div>';
                if (!isFirst) {
                    html += '<div class="apw-sudut-control"><label>Sudut</label>';
                    html +=
                        '<input type="range" min="-180" max="180" value="' +
                        s.sudut +
                        '" step="1" class="apw-sudut-slider" data-index="' +
                        i +
                        '">';
                    html += '<span class="apw-sudut-value" data-index="' + i + '">' + s.sudut + "&deg;</span></div>";
                } else {
                    html +=
                        '<div style="min-width:140px;color:#2f5e8a;font-weight:500;background:#e2f0fa;padding:6px 18px;border-radius:30px;">&#8614; arah awal 0&deg;</div>';
                }
                html += "</div>";
            }
            segmenListDiv.innerHTML = html;
            document.querySelectorAll(".apw-bobot-input").forEach(function (inp) {
                inp.addEventListener("input", function () {
                    var idx = parseInt(this.dataset.index);
                    var val = parseInt(this.value);
                    if (isNaN(val) || val < 1) val = 1;
                    if (val > 1000) val = 1000;
                    segmen[idx].bobot = val;
                    updateTotalBobot();
                    drawScene();
                });
            });
            document.querySelectorAll(".apw-sudut-slider").forEach(function (slider) {
                slider.addEventListener("input", function () {
                    var idx = parseInt(this.dataset.index);
                    var val = parseInt(this.value);
                    segmen[idx].sudut = val;
                    var sp = document.querySelector('.apw-sudut-value[data-index="' + idx + '"]');
                    if (sp) sp.innerText = val + "\u00b0";
                    drawScene();
                });
            });
            updateTotalBobot();
            hapusSegmen.disabled = segmen.length <= 1;
        }

        function updateTotalBobot() {
            var total = segmen.reduce(function (acc, s) {
                return acc + s.bobot;
            }, 0);
            totalBobotSpan.innerText = total;
        }

        function tambahRuas() {
            if (segmen.length >= MAX_SEGMEN) {
                alert("Maksimal " + MAX_SEGMEN + " ruas.");
                return;
            }
            segmen.push({ bobot: 30, sudut: 0 });
            renderSegmenList();
            drawScene();
        }

        function hapusRuasTerakhir() {
            if (segmen.length <= 1) return;
            segmen.pop();
            renderSegmenList();
            drawScene();
        }

        function hitungTitik(startX, startY) {
            var totalBobot = segmen.reduce(function (acc, s) {
                return acc + s.bobot;
            }, 0);
            if (totalBobot === 0) return [{ x: startX, y: startY }];
            var points = [{ x: startX, y: startY }];
            var arahKum = 0;
            var currentX = startX;
            var currentY = startY;
            for (var i = 0; i < segmen.length; i++) {
                var s = segmen[i];
                var panjangRuas = panjangTotal * (s.bobot / totalBobot);
                if (i > 0) arahKum += (s.sudut * Math.PI) / 180;
                currentX += panjangRuas * Math.cos(arahKum);
                currentY += panjangRuas * Math.sin(arahKum);
                points.push({ x: currentX, y: currentY });
            }
            return points;
        }

        function hitungPanjangLintasan() {
            var totalBobot = segmen.reduce(function (acc, s) {
                return acc + s.bobot;
            }, 0);
            return totalBobot > 0 ? panjangTotal : 0;
        }

        function getPointAndAngleAtProgress(progress) {
            var startX = 240,
                startY = 180;
            var points = hitungTitik(startX, startY);
            if (points.length < 2) return { x: startX, y: startY, angle: 0 };
            var totalBobot = segmen.reduce(function (acc, s) {
                return acc + s.bobot;
            }, 0);
            if (totalBobot === 0) return { x: startX, y: startY, angle: 0 };
            var panjangRuasList = segmen.map(function (s) {
                return panjangTotal * (s.bobot / totalBobot);
            });
            var totalPanjang = panjangRuasList.reduce(function (a, b) {
                return a + b;
            }, 0);
            var targetPanjang = progress * totalPanjang;
            if (targetPanjang < 0) return { x: points[0].x, y: points[0].y, angle: 0 };
            if (targetPanjang >= totalPanjang) {
                var last = points.length - 1;
                return { x: points[last].x, y: points[last].y, angle: 0 };
            }
            var accum = 0;
            for (var i = 0; i < panjangRuasList.length; i++) {
                var pj = panjangRuasList[i];
                if (targetPanjang <= accum + pj || i === panjangRuasList.length - 1) {
                    var sisa = targetPanjang - accum;
                    var t = pj === 0 ? 0 : sisa / pj;
                    var x = points[i].x + (points[i + 1].x - points[i].x) * t;
                    var y = points[i].y + (points[i + 1].y - points[i].y) * t;
                    var angle = Math.atan2(points[i + 1].y - points[i].y, points[i + 1].x - points[i].x);
                    return { x: x, y: y, angle: angle };
                }
                accum += pj;
            }
            return { x: points[points.length - 1].x, y: points[points.length - 1].y, angle: 0 };
        }

        function drawScene() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            var startX = 240,
                startY = 180;
            var points = hitungTitik(startX, startY);
            var blink = 0.4 + 0.6 * Math.sin(Date.now() / 200);
            ctx.save();
            ctx.globalAlpha = blink;
            if (points.length >= 2) {
                ctx.beginPath();
                ctx.moveTo(points[0].x, points[0].y);
                for (var i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
                ctx.strokeStyle = "#c2185b";
                ctx.lineWidth = 4.5;
                ctx.lineJoin = "round";
                ctx.lineCap = "round";
                ctx.shadowColor = "#b0bec5";
                ctx.shadowBlur = 8;
                ctx.stroke();
                ctx.shadowBlur = 0;
            }
            for (var i = 0; i < points.length; i++) {
                ctx.beginPath();
                ctx.arc(points[i].x, points[i].y, i === 0 || i === points.length - 1 ? 6 : 5, 0, 2 * Math.PI);
                ctx.fillStyle = i === 0 ? "#0b5e42" : i === points.length - 1 ? "#b03e3e" : "#1976d2";
                ctx.shadowBlur = 8;
                ctx.shadowColor = "#333";
                ctx.fill();
            }
            ctx.shadowBlur = 0;
            ctx.restore();
            var totalPanjang = hitungPanjangLintasan();
            if (totalPanjang === 0) return;
            var step = 1 / jumlahPanah;
            for (var i = 0; i < jumlahPanah; i++) {
                var p = progressUtama - i * step;
                p = p - Math.floor(p);
                var pt = getPointAndAngleAtProgress(p);
                var orientasi = arahPanah === 1 ? pt.angle : pt.angle + Math.PI;
                ctx.save();
                ctx.translate(pt.x, pt.y);
                ctx.rotate(orientasi);
                ctx.fillStyle = "#FFD966";
                ctx.shadowColor = "black";
                ctx.shadowBlur = 8;
                ctx.beginPath();
                ctx.moveTo(12, 0);
                ctx.lineTo(-8, -6);
                ctx.lineTo(-8, 6);
                ctx.closePath();
                ctx.fill();
                ctx.restore();
            }
            ctx.font = 'bold 12px "Segoe UI",monospace';
            ctx.fillStyle = "#1a2c3f";
            ctx.fillText("progress: " + (progressUtama * 100).toFixed(1) + "%", 12, 30);
            ctx.fillText("arah: " + (arahPanah === 1 ? "\u2192" : "\u2190"), 12, 55);
            ctx.fillText("panah: " + jumlahPanah, 12, 80);
        }

        function animationStep() {
            if (!animActive) return;
            progressUtama += SPEED * arahPanah;
            if (progressUtama > 1.0) progressUtama -= 1.0;
            else if (progressUtama < 0.0) progressUtama += 1.0;
            drawScene();
            animId = requestAnimationFrame(animationStep);
        }

        function startAnimation() {
            if (!animActive) {
                animActive = true;
                if (animId) cancelAnimationFrame(animId);
                animId = requestAnimationFrame(animationStep);
            }
        }

        function stopAnimation() {
            if (animActive) {
                animActive = false;
                if (animId) {
                    cancelAnimationFrame(animId);
                    animId = null;
                }
                drawScene();
            }
        }

        panjangSlider.addEventListener("input", function () {
            panjangTotal = parseInt(this.value);
            panjangDisplay.innerText = panjangTotal + " px";
            drawScene();
        });
        forwardRadio.addEventListener("change", function () {
            if (forwardRadio.checked) arahPanah = 1;
            drawScene();
        });
        backwardRadio.addEventListener("change", function () {
            if (backwardRadio.checked) arahPanah = -1;
            drawScene();
        });
        jmlPanahSlider.addEventListener("input", function () {
            jumlahPanah = parseInt(this.value);
            jmlPanahValue.innerText = jumlahPanah;
            drawScene();
        });
        startBtn.addEventListener("click", function () {
            arahPanah = forwardRadio.checked ? 1 : -1;
            startAnimation();
        });
        stopBtn.addEventListener("click", stopAnimation);
        resetBtn.addEventListener("click", function () {
            progressUtama = 0.0;
            if (!animActive) drawScene();
        });
        tambahSegmen.addEventListener("click", tambahRuas);
        hapusSegmen.addEventListener("click", hapusRuasTerakhir);

        renderSegmenList();
        panjangDisplay.innerText = panjangTotal + " px";
        jmlPanahValue.innerText = jumlahPanah;
        drawScene();
    })();
</script>

Comments