import {GUI} from '../../examples/jsm/libs/lil-gui.module.min.js'; { function outlineText(ctx, msg, x, y) { ctx.strokeText(msg, x, y); ctx.fillText(msg, x, y); } function arrow(ctx, x1, y1, x2, y2, start, end, size) { size = size || 1; const dx = x1 - x2; const dy = y1 - y2; const rot = -Math.atan2(dx, dy); const len = Math.sqrt(dx * dx + dy * dy); ctx.save(); { ctx.translate(x1, y1); ctx.rotate(rot); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, -(len - 10 * size)); ctx.stroke(); } ctx.restore(); if (start) { arrowHead(ctx, x1, y1, rot, size); } if (end) { arrowHead(ctx, x2, y2, rot + Math.PI, size); } } function arrowHead(ctx, x, y, rot, size) { ctx.save(); { ctx.translate(x, y); ctx.rotate(rot); ctx.scale(size, size); ctx.translate(0, -10); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(-5, -2); ctx.lineTo(0, 10); ctx.lineTo(5, -2); ctx.closePath(); ctx.fill(); } ctx.restore(); } const THREE = { MathUtils: { radToDeg(rad) { return rad * 180 / Math.PI; }, degToRad(deg) { return deg * Math.PI / 180; }, }, }; class DegRadHelper { constructor(obj, prop) { this.obj = obj; this.prop = prop; } get value() { return THREE.MathUtils.radToDeg(this.obj[this.prop]); } set value(v) { this.obj[this.prop] = THREE.MathUtils.degToRad(v); } } function dot(x1, y1, x2, y2) { return x1 * x2 + y1 * y2; } function distance(x1, y1, x2, y2) { const dx = x1 - x2; const dy = y1 - y2; return Math.sqrt(dx * dx + dy * dy); } function normalize(x, y) { const l = distance(0, 0, x, y); if (l > 0.00001) { return [x / l, y / l]; } else { return [0, 0]; } } function resizeCanvasToDisplaySize(canvas, pixelRatio = 1) { const width = canvas.clientWidth * pixelRatio | 0; const height = canvas.clientHeight * pixelRatio | 0; const needResize = canvas.width !== width || canvas.height !== height; if (needResize) { canvas.width = width; canvas.height = height; } return needResize; } const diagrams = { dotProduct: { create(info) { const {elem} = info; const div = document.createElement('div'); div.style.position = 'relative'; div.style.width = '100%'; div.style.height = '100%'; elem.appendChild(div); const ctx = document.createElement('canvas').getContext('2d'); div.appendChild(ctx.canvas); const settings = { rotation: 0.3, }; const gui = new GUI({autoPlace: false}); gui.add(new DegRadHelper(settings, 'rotation'), 'value', -180, 180).name('rotation').onChange(render); gui.domElement.style.position = 'absolute'; gui.domElement.style.top = '0'; gui.domElement.style.right = '0'; div.appendChild(gui.domElement); const darkColors = { globe: 'green', camera: '#AAA', base: '#DDD', label: '#0FF', }; const lightColors = { globe: '#0C0', camera: 'black', base: '#000', label: 'blue', }; const darkMatcher = window.matchMedia('(prefers-color-scheme: dark)'); darkMatcher.addEventListener('change', render); function render() { const {rotation} = settings; const isDarkMode = darkMatcher.matches; const colors = isDarkMode ? darkColors : lightColors; const pixelRatio = window.devicePixelRatio; resizeCanvasToDisplaySize(ctx.canvas, pixelRatio); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.save(); { const width = ctx.canvas.width / pixelRatio; const height = ctx.canvas.height / pixelRatio; const min = Math.min(width, height); const half = min / 2; const r = half * 0.4; const x = r * Math.sin(-rotation); const y = r * Math.cos(-rotation); const camDX = x - 0; const camDY = y - (half - 40); const labelDir = normalize(x, y); const camToLabelDir = normalize(camDX, camDY); const dp = dot(...camToLabelDir, ...labelDir); ctx.scale(pixelRatio, pixelRatio); ctx.save(); { { ctx.translate(width / 2, height / 2); ctx.beginPath(); ctx.arc(0, 0, half * 0.4, 0, Math.PI * 2); ctx.fillStyle = colors.globe; ctx.fill(); ctx.save(); { ctx.fillStyle = colors.camera; ctx.translate(0, half); ctx.fillRect(-15, -30, 30, 30); ctx.beginPath(); ctx.moveTo(0, -25); ctx.lineTo(-25, -50); ctx.lineTo( 25, -50); ctx.closePath(); ctx.fill(); } ctx.restore(); ctx.save(); { ctx.lineWidth = 4; ctx.strokeStyle = colors.camera; ctx.fillStyle = colors.camera; arrow(ctx, 0, half - 40, x, y, false, true, 2); ctx.save(); { ctx.strokeStyle = colors.label; ctx.fillStyle = colors.label; arrow(ctx, 0, 0, x, y, false, true, 2); } ctx.restore(); { ctx.lineWidth = 3; ctx.strokeStyle = 'black'; ctx.fillStyle = dp < 0 ? 'white' : 'red'; ctx.font = '20px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; outlineText(ctx, 'label', x, y); } } ctx.restore(); } ctx.restore(); } ctx.lineWidth = 3; ctx.font = '24px sans-serif'; ctx.strokeStyle = 'black'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.save(); { ctx.translate(width / 4, 80); const textColor = dp < 0 ? colors.base : 'red'; advanceText(ctx, textColor, 'dot( '); ctx.save(); { ctx.fillStyle = colors.camera; ctx.strokeStyle = colors.camera; ctx.rotate(Math.atan2(camDY, camDX)); arrow(ctx, -8, 0, 8, 0, false, true, 1); } ctx.restore(); advanceText(ctx, textColor, ' , '); ctx.save(); { ctx.fillStyle = colors.label; ctx.strokeStyle = colors.label; ctx.rotate(rotation + Math.PI * 0.5); arrow(ctx, -8, 0, 8, 0, false, true, 1); } ctx.restore(); advanceText(ctx, textColor, ` ) = ${dp.toFixed(2)}`); } ctx.restore(); } ctx.restore(); } render(); window.addEventListener('resize', render); }, }, }; function advanceText(ctx, color, str) { ctx.fillStyle = color; ctx.fillText(str, 0, 0); ctx.translate(ctx.measureText(str).width, 0); } [...document.querySelectorAll('[data-diagram]')].forEach(createDiagram); function createDiagram(base) { const name = base.dataset.diagram; const info = diagrams[name]; if (!info) { throw new Error(`no diagram ${name}`); } info.create({elem:base}); } }