konvajs

Stars and mentions

Last 30 days, Oct 5 – Nov 3

GitHub stars58
mentions3
02468Oct 5Oct 19Nov 3
UTC days · repositories konvajs owns2 repositories

Package downloads

Cumulative recorded downloads · since 2025-10-20

npm81.1M
020.3M40.6M60.8M81.1MOct 5Oct 19Nov 3
Oct 27npm: 1/2 packages observed

Across 2 known packages in repositories konvajs owns. Cumulative totals include recorded downloads before the selected range. Missing observations leave gaps; totals may be incomplete.

Top repositories

konvajs’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside konvajs’s account where konvajs opened or reviewed pull requests in the past 365 days.

Who noticed konvajs

Everyone who starred or forked konvajs’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

3posts
Nov 40 posts
  1. Why brightness and contrast order mattered: correcting a browser editor’s pixel response A brightness slider can work perfectly as UI while producing the wrong pixels. In Photoshot, my browser image editor, the difficult part was matching a measured brightness/contrast response while keeping the adjustment non-destructive and integrated with masks, opacity and saved projects. This is a compact account of the reference code, the numerical failure, the correction, and what was actually verified. 1. The first implementation and its reference The September 18 development record shows Konva's src/filters/Contrast.ts being inspected before the first adjustment engine was written. Its midpoint-centered pixel loop was useful for understanding the channel range and the fact that contrast need not modify alpha. Photoshot used separately written expressions: brighten toward white or darken toward black, then apply contrast around normalized midpoint 0.5. The original-revision branch remains in the source: if (t === 'brightness' && a.revision !== 2) { x = v.brightness < 0 ? x * (1 + v.brightness / 100) : x + ((1 - x) * v.brightness) / 100; const contrast = v.contrast < 0 ? 1 + v.contrast / 100 : 1 / (1 - v.contrast / 101); x = (x - 0.5) * contrast + 0.5; } The engine already compiled three 256-entry channel lookup tables. The later correction changed their response calculation, not the existence of lookup tables. 2. The measured failure On September 25, an intermediate normal-mode approximation was compared with Photoshop 27.3.1 at brightness +30 and contrast +20. It produced mean absolute RGB-channel error 11.505 and maximum error 25. Two retained gray samples make the failure concrete: | Input | Intermediate output | Archived Photoshop output | | --- | --- | --- | | 0 | 25 | 0 | | 128 | 168 | 157 | Those errors belong to the intermediate build used in that comparison. They must not be presented as a measurement of the first September 18 legacy formula. !Archived mean and maximum errors before correction and with both response orders Historical chart measurements at +30/+20; each bar is one deterministic evaluation. 3. Why consulting GitHub was not the entire solution Konva was a consulted source, not an installed, verbatim replacement for our filter. A public contrast implementation may target a different response curve. It also does not supply our adjustment-layer serialization, mask integration or old-project behavior. The actual chronology was reference lookup → application implementation → later comparison → calibration. The reference was not first discovered after the September 25 failure. 4. Replace the approximation with sampled responses Calibration used a synthetic gray ramp in Photoshop normal mode. Brightness was sampled from −150 to +150 and contrast from −50 to +100, in steps of 10, with the other control held at zero. The data are retained in tone-response.ts. sampleResponse interpolates intermediate settings and channel positions. Revision 2 normal mode applies brightness first, then contrast: x = sampleResponse(toneResponse.brightness, v.brightness, -150, x); x = sampleResponse(toneResponse.contrast, v.contrast, -50, x); The order is meaningful: C(B(x)) generally differs from B(C(x)). The result still flows through the existing floating-point channel tables. Masks and opacity remain compositor responsibilities; old revision behavior and the separate Legacy option remain distinct branches. 5. The result—and a control that reverses the order The historical chart was 256 × 32 pixels: eight gray-ramp rows followed by mixed RGB rows. Alpha was excluded, leaving 24,576 channel samples. | Historical implementation, +30/+20 | Mean absolute error | Maximum | | --- | ---: | ---: | | Intermediate approximation | 11.505 | 25 | | Measured response, brightness → contrast | 0.241211 | 1 | | Measured response, contrast → brightness | 1.149740 | 4 | For that chart condition, the mean error fell by approximately 97.9% relative to the intermediate approximation. That is an error reduction, not a universal compatibility or visual-quality score. Each row is one deterministic chart evaluation, so there is no invented confidence interval or speed claim. !Archived failed gray values, Photoshop comparison points and the current source replay Numeric records visualized as patches and curves; historical measurements and the new replay remain distinct. 6. What the current rerun verifies The unchanged public engine's response tables were compared with the archived calibration JSON and match exactly. A fresh source-level replay then evaluated nine retained gray inputs at each of +30/+20, +17/+13 and −65/−23: 27/27 outputs equal the archived Photoshot outputs. That does not mean 27/27 equal Photoshop exactly; some retained Photoshop comparisons differ by one channel value. Nor does this rerun recompute the full-chart means: the original reference PNGs were unavailable in the reviewed folder. Historical chart statistics, archived Photoshop samples and the new replay are deliberately separated. Sources and full figures - Konva Contrast.ts: consulted reference - Photoshot adjustment engine at 19f45ed - Measured response data and interpolation For the full version, including the pipeline diagram, before/after error charts, gray-value comparisons and reproducibility files, read the complete research note on my website. Writing and source review were assisted by AI. Claims above are tied to the cited source and retained measurement records.

  2. 倉庫UIの複数選択を、Konvaで「なぞる」操作にした 倉庫マップには、数百個の置場が小さな矩形として並びます。一つずつタップして状態を変えるのは遅いため、指で置場をなぞって選べる操作を作りました。 KonvaのTransformerで矩形範囲を囲む方式ではなく、stage.getIntersection()で指の真下にある図形を拾います。実装の要点は、5pxの閾値、ドラッグ開始時に決める選択・解除モード、同じ置場を二度処理しないためのSetの3つです。 数百個の置場を一つずつ押したくない 私がある製造業のお客様向けに作った倉庫監視 UI は、技術スタックが Nuxt 3 + Vue 3 + Konva.js(vue-konva でラップ)です。その中の中心的なインタラクションが、倉庫内の数百個の置場(rack)を 700×650 の canvas 上に密に並んだ小さな矩形として描き、オペレーターが一度に一群を選んで状態をまとめて変更できる、というものでした。 一つずつクリックするのは現実的でないので、複数選択は必須要件です。ただ最終的に、私は Konva 標準の「rubber-band 選択 + Transformer」の道は採らず、自前でなぞり式(paint)の複数選択を書きました——押したままなぞると、ポインターの真下にある rack が選択集合に加わっていく方式です。 この記事では、この設計上の選択、実際の状態機の実装、ハマったところ、そしてもう一つのフォールバックであるレンジ選択まで一緒に書いていきます。 背景:なぜ rubber-band 選択を使わなかったか Konva で複数選択をするとき、ネットで一番よく見るやり方は、半透明の矩形(rubber-band)をドラッグで描き、離したときにその矩形の getClientRect() を各 shape と Util.haveIntersection で当たり判定して、ヒットしたものに Transformer をかける、というものです。枯れたやり方で、ドキュメントも揃っています。 ただ、この倉庫UIには合わないと感じました。理由は三つあります。 倉庫マップは密に並んだグリッドです。数百個の矩形が隣り合って並んでいて、オペレーターのメンタルモデルは「この一列、このひと塊を塗りたい」であって、「ある領域を囲みたい」ではありません。密なグリッドの上では、「なぞる」方が「囲む」より直感的です——Excel でセルをドラッグして一帯を選ぶ感覚に近い。 対象デバイスは工業用タッチパネル / iPadです。タッチ画面で矩形枠をドラッグすると、ページ / canvas 自体のスクロールやズームのジェスチャーとぶつかりやすい。一方「一つの rack を押したまま横へなぞる」という動作は、意味的に「これらのマスの上をなぞっている」に近く、スクロールとの衝突が小さい。 rubber-band はさらに、選択枠の shape を自分で持って、それと各 rack の交差を計算する必要があります。shape が増えれば haveIntersection もひと回りループです。私が本当に気にしているのはポインターの真下にあるただ一つだけなのに。 特に効いたのがstage.getIntersection(pos)です。座標を渡すと、その点の最前面にあるshapeを返します。今回必要なのはポインター直下の置場一つだけなので、矩形との交差判定を自前で書く必要はありません。 なぞり式複数選択の実装 状態機:6 つの変数 ドラッグのロジック全体は、下の数個の ref で支えられています。これ以上凝ったものはありません。 // スライド選択の状態 const isDragging = ref(false); const dragStartPos = ref<{ x: number; y: number } | null>(null); const hasMoved = ref(false); // 実際に動いたかどうかのフラグ const dragMode = ref<"select" | "deselect">("select"); // 選択 / 選択解除 const processedRacksInDrag = ref<Set<string>>(new Set()); // 今回のドラッグで処理済みの置場 const DRAG_THRESHOLD = 5; // 5px 以上動いて初めてドラッグとみなす dragMode とこの Set が、この設計で一番語る価値のある二つで、後でそれぞれ分けて説明します。 イベント側は、マウスとタッチを同じ handler のセットにバインドしました——対象デバイスには工業用タッチパネルもあれば、デスクトップとして使う場面もあるので、二度書く必要はありません。 <v-layer @mousedown="handleDragStart" @mousemove="handleDragMove" @mouseup="handleDragEnd" @touchstart="handleDragStart" @touchmove="handleDragMove" @touchend="handleDragEnd" > 押下:まずこの一筆が「選択」か「解除」かを決める 押した瞬間に、当時は当たり前と思っていて、後になってかなり重要だと気づいたことをしています:始点の rack が今選択済みかどうかで、このドラッグ一筆全体を select か deselect にロックするのです。 const handleDragStart = (e: any) => { const stage = e.target.getStage(); if (!stage) return; const pos = stage.getPointerPosition(); if (!pos) return; // タッチでは座標が取れないことがあるので一手防いでおく // 始点の真下の置場を一目見て、この一筆のモードを決める const shape = stage.getIntersection(pos); if (shape) { const parkNo = shape.id(); if (parkNo) { const rack = findRackByParkNo(parkNo); if (rack) { // 下段 -1 / 上段 -2、一回の操作で両段を持つ const layer1ParkNo = parkNo.replace(/-2$/, "-1"); const layer2ParkNo = parkNo.replace(/-1$/, "-2"); const existing = updateRacks.value.find( (item) => item.parkNo === layer1ParkNo || item.parkNo === layer2ParkNo); dragMode.value = existing ? "deselect" : "select"; // 始点が選択済み → この一筆は全部解除 } } } dragStartPos.value = { x: pos.x, y: pos.y }; hasMoved.value = false; isDragging.value = true; processedRacksInDrag.value.clear(); // ドラッグごとに重複用 Set をクリア }; なぜ始点でモードを固定するのか? ロックしないと、選択済みのものをなぞり、続けて未選択のものをなぞったとき、選択なのか解除なのか乱れてしまうからです——同じ一筆の中で選択したり解除したりが入り混じり、挙動が暴れて見えます。「始点が未選択 → この一筆は選択だけ、始点が選択済み → この一筆は解除だけ」とロックすれば、ジェスチャー全体の意味が安定します。 移動:閾値 → ヒット → 重複排除 handleDragMove の中では、順に三つの関門を通します。 const handleDragMove = (e: any) => { if (!isDragging.value) return; const stage = e.target.getStage(); const pos = stage.getPointerPosition(); if (!pos || !dragStartPos.value) return; // 5px 未満ならクリックとして扱う const dx = pos.x - dragStartPos.value.x; const dy = pos.y - dragStartPos.value.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < DRAG_THRESHOLD) return; if (!hasMoved.value) hasMoved.value = true; // ポインター直下の shape は Konva に探させる const shape = stage.getIntersection(pos); if (!shape) return; const parkNo = shape.id(); // このドラッグですでに処理した置場は飛ばす if (!parkNo || processedRacksInDrag.value.has(parkNo)) return; const rack = findRackByParkNo(parkNo); if (!rack) return; if (!canSelectRack(rack)) return; // 選択可否のフィルタ(占有 / 禁止など) // …dragMode に従って、この rack の下段 -1 / 上段 -2 を一緒に push / filter… processedRacksInDrag.value.add(parkNo); processedRacksInDrag.value.add(layer1ParkNo); processedRacksInDrag.value.add(layer2ParkNo); }; 三つの関門のうち、ヒット判定のあの一行こそが設計全体の核心です。私は「点が矩形の中にあるか」の幾何計算は一切書かず、stage.getIntersection(pos) の一行で済ませています。返ってくるのはこの座標点の上、最前面の shape で、その id() がそのまま parkNo です。 ここには業務上の制約もあります。倉庫のラックは二段で、下段のparkNoは-1、上段は-2で終わります。一回の操作では両段を一緒に処理します。replace(/-2$/, "-1")とreplace(/-1$/, "-2")で対応する番号を求め、ペアで追加または削除します。片方だけ更新すると状態がずれるためです。 押上:きれいに片付ける const handleDragEnd = () => { isDragging.value = false; dragStartPos.value = null; hasMoved.value = false; dragMode.value = "select"; // デフォルトモードに戻す processedRacksInDrag.value.clear(); }; 何の工夫もなく、次の一筆に持ち越さないよう状態機をリセットするだけです。 Setで同じ置場の再処理を防ぐ mousemoveとtouchmoveにはthrottleもdebounceもかけていません。代わりに、processedRacksInDragというSetで、同じドラッグ中に同じ置場を繰り返し更新しないようにしています。 mousemoveは高頻度で発火します。同じ置場の上で指が少し揺れるだけでも、同じshapeを何度も拾います。そのたびにupdateRacksを更新すると、同じ置場が繰り返し追加・削除され、選択状態まで揺れてしまいます。 最初になぞったときだけ処理してSetへ記録し、同じparkNoを再び拾ったらSet.has()で飛ばします。これは描画全体を高速化する仕組みではありません。選択配列への重複更新を防ぎ、ジェスチャーの意味を安定させるための仕組みです。 ハイライト:fill を切り替え、枠は付けない rubber-band のやり方は普通、選択した要素に Transformer で枠線を描きます。私の方は Transformer がそもそも無いので、ハイライトは色で直接やります——v-rect の fill を reactive な関数にバインドします。 const handleColor = (rack: BasicParkInfo) => { // 選択集合の中 → オレンジでハイライト if (updateRacks.value.find((item) => item.parkNo === rack.parkNo)) return COLORS.second; // #ff7f00 if (rack.unusable === 1) return COLORS.warning; // 禁止 if (rack.cargoStatus !== 0) return COLORS.twoOccupied; // 荷あり return COLORS.emptyStatus; // 空 }; updateRacks(選択集合)が変わると、すべての v-rect の handleColor が計算し直され、選ばれたマスはすぐに #ff7f00 になります。利点は、ハイライトと「選択されているか」が同じデータソースで、Transformer ノードの一群を別途維持しなくていいこと。代償は後のハマりどころで触れますが——マスが多いとこの再計算にはコストがあります。 ハマったところいくつか クリックとドラッグが同じ要素を取り合う。 同じ v-rect の上に、@click / @tap の単一選択と、外側 layer の mousedown→move→up の複数選択が両方あります。 @tap="canSelectRack(rack) && toggleSelection(rack.parkNo)" @click="canSelectRack(rack) && toggleSelection(rack.parkNo)" この二つがどうぶつからないか? すべてあの 5px 閾値のおかげです。指を押してほとんど動かさず(変位 < 5px)に離すと、handleDragMove 内の距離判定がずっと return し、hasMoved は false のままなので、ドラッグは実質何もせず、@click の単一選択だけが効きます。閾値のこの数字は当時調整しました:小さすぎると、手が少し震えただけでドラッグ扱いになって一帯を誤選択する。大きすぎると、なぞり始めにそれなりに動かさないと反応せず、操作感が鈍い。5px は試行で出した、タッチ画面で操作感が安定する値です。 タッチ画面では getPointerPosition() が null を返す。 これは忘れやすいポイントです。タッチイベントは特定のタイミングで座標が取れないので、各 handler の冒頭で if (!pos) return を一枚挟まないと、後の pos.x がそのまま落ちます。 二段ラックの同期。 先ほどの-1 / -2のペア処理は、当時の業務ルールに合わせたものです。どこか一か所でも対応するもう一段を更新し忘れると、二段の状態がずれてしまいます。そのため、どの分岐でも二つのparkNoを一緒にSetへ入れ、同時に選択集合へ追加します。 fill 切り替えの再描画コスト。 ハイライトは「選択集合が変わる → すべての v-rect が handleColor を再計算」なので、マスが特に多い拠点では、一回の選択で広範囲の再評価が走ります。ここは後で別の Konva 性能の記事でもっと体系的に扱いましたが、ここでは注意点として:fill でハイライトすると Transformer を省けますが、再描画コストがあることは忘れないでください。 もう一つの方式:2 回クリックの一行レンジ選択 なぞり式は「ある形をきっちり塗り出したい」ときは手に馴染みますが、ドラッグが不便な場面もあります——たとえば「この一行の 3 列目から 18 列目まで」を選びたいとき、なぞるのは遅いし斜めにずれやすい。そこで別のコンポーネントに、クリックベースのレンジ選択をもう一つ作り、「複数選択モード」のスイッチで切り替えるようにしました。 その状態機はとても単純で、「最初の一点」を覚えておくだけです。 const multiSelectMode = ref(false); const firstSelectedLocation = ref<{ parkNo: string; rowKey: string; colKey: string } | null>(null); クリックの処理は二段式です。 const toggleSelection = (/* parkNo, rowKey, colKey, … */) => { if (multiSelectMode.value) { if (!firstSelectedLocation.value) { // 1 回目のクリック:始点を記録 firstSelectedLocation.value = { parkNo, rowKey, colKey }; toggleSingleSelection(/* … */); } else { // 2 回目のクリック:同じ行 → レンジ選択;違う行 → 通常の単一選択扱い if (firstSelectedLocation.value.rowKey === rowKey) { handleRangeSelection(firstSelectedLocation.value.colKey, colKey, rowKey); } else { toggleSingleSelection(/* … */); } firstSelectedLocation.value = null; // 使ったらクリアして次のペアに備える } return; } toggleSingleSelection(/* … */); // 複数選択モードでない:純粋な単一選択 }; レンジ展開の実体は列の index で行います。ここに当時危うく忘れかけたディテールが一つ——両方向のクリックに対応する必要があります:先に右を点いてから左、と先に左を点いてから右で、結果は同じであるべき。Math.min/max で起点・終点の index を正しい向きに揃えます。 const handleRangeSelection = (startCol, endCol, rowKey) => { const allColKeys = Object.keys(rowData).sort((a, b) => parseInt(a) - parseInt(b)); const startIndex = allColKeys.indexOf(startCol); const endIndex = allColKeys.indexOf(endCol); const minIndex = Math.min(startIndex, endIndex); // 両方向どちらも正しく const maxIndex = Math.max(startIndex, endIndex); // shouldSelect:始点が選択済みか、で この区間を「全選択」か「全解除」かを決める for (let i = minIndex; i <= maxIndex; i++) { // …範囲内を列ごとに push / filter… } }; ここはなぞり式のあの dragMode と同じ発想です:始点の状態で区間全体を選択か解除か一度に決め、途中で変えない。このほかに group モードもあって、行頭を点くとその行の rack をまとめて push します。ロジックは同じ理屈(一行の全列を flat() で出し、from/to/状態変更の可否でフィルタしてから一括で選択集合へ)なので、貼りません。 選び終わると、どちらの方式も emit("selectedItems", …) で選択集合を親コンポーネントへ投げ、最後に確定ボタンが一括で WCS(倉庫制御システム)の確定 API へ送ります——選択するレイヤーと送信するレイヤーは分かれていて、UI は選択集合を貯めることだけを担います。 実装して分かったこと Transformerとrubber-bandは複数選択の定番ですが、密なグリッドをタッチ操作する今回の画面では、なぞる方式のほうが要件に合いました。 ヒット判定はすべて stage.getIntersection(pos) に任せ、幾何の交差を自分で計算しない。だから選択枠の shape も要らない。 5px 閾値でクリックとドラッグを一刀両断にし、単一選択と複数選択を同じ要素の上に共存させる。 押した時に始点の状態で dragMode をロックし、一筆のジェスチャーの意味を安定させる。 throttleは入れず、Setで同じ置場への重複更新を防ぐ。 ハイライトは reactive な fill の切り替えで行い、Transformer の枠線にはしない。 ドラッグが手に馴染まない場面は、2 回クリックの一行レンジ選択でフォローする。 もし続けて改善するなら、次はこの状態機をコンポーネントからcomposable に切り出すでしょう——isDragging / dragMode / processedRacksInDrag のこれらの変数と三つの handler は、それ自体で完結した小さなユニットなので、切り出せば別の canvas に再利用でき、単体でテストもしやすくなります。 参考リンク Konva.js — Stage.getIntersection()(公式 API) Konva.js — Select and Transform(公式 rubber-band サンプル) vue-konva(公式) Vue 3 — Composables(公式)

  3. konva ⭐ 15k Konva is a JavaScript library that lets developers draw shapes, layers, and handle events on HTML5 canvas. 🔗 konvajs/konva #opensource t.co

    2 views

Achievements

Awards earned by the repositories konvajs owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.