- Get link
- X
- Other Apps
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
Ruas garis & sudut belok
⚡ aliran
✨ Bobot total: 0
(dinormalkan ke 100%)
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 & 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 + "°</span></div>"; } else { html += '<div style="min-width:140px;color:#2f5e8a;font-weight:500;background:#e2f0fa;padding:6px 18px;border-radius:30px;">↦ arah awal 0°</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