In case Flash no longer exists; a copy of this site is included in the Flashpoint archive's "ultimate" collection.

Dead Code Preservation :: Archived AS3 works from wonderfl.net

Link Arrow Test

Sprite の Link と接点の修飾を行うテストコード 2010/08/21

今後 DisplayObject の Link に対する ArrowShape クラスなりを用意して
Tail / Head の描画を柔軟に切り替えられれば実用度も高まりそう?
あと、Link の線自体の引き方も曲線や直角をサポートするべきか?

グラフの各要素に対する制御は Flash だと意識しなくても DisplayObject を
ベースにすれば事足りそう。他の言語使うより、こりゃ楽だわw
この手の有向グラフに対する線の描画補助って、需要あったりするのだろうか?

今回から接点のチェック方法はブレゼンハムを使うようにしているため
図形が回転していても、それなりの精度で矢印を描画してくれる。

接点の修飾に関しては、同じ接続の場合、線の集約も考える必要が有るだろうか?
集約すると表現としてマズい場合もあるだろうから、可変できる必要もあるだろう。
いろいろ考えるとムツカシイ ^^;
/**
 * Copyright zier ( http://wonderfl.net/user/zier )
 * MIT License ( http://www.opensource.org/licenses/mit-license.php )
 * Downloaded from: http://wonderfl.net/c/gDNR
 */

// forked from zier's forked from: forked from: Sprite Link Test
// forked from zier's forked from: Sprite Link Test
// forked from zier's Sprite Link Test
package {
/* Sprite の Link と接点の修飾を行うテストコード 2010/08/21
 *
 * 今後 DisplayObject の Link に対する ArrowShape クラスなりを用意して
 * Tail / Head の描画を柔軟に切り替えられれば実用度も高まりそう?
 * あと、Link の線自体の引き方も曲線や直角をサポートするべきか?
 *
 * グラフの各要素に対する制御は Flash だと意識しなくても DisplayObject を
 * ベースにすれば事足りそう。他の言語使うより、こりゃ楽だわw
 * この手の有向グラフに対する線の描画補助って、需要あったりするのだろうか?
 *
 * 今回から接点のチェック方法はブレゼンハムを使うようにしているため
 * 図形が回転していても、それなりの精度で矢印を描画してくれる。
 *
 * 接点の修飾に関しては、同じ接続の場合、線の集約も考える必要が有るだろうか?
 * 集約すると表現としてマズい場合もあるだろうから、可変できる必要もあるだろう。
 * いろいろ考えるとムツカシイ ^^;
 */

    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.MouseEvent;
    
    [SWF(width = 400, height = 400, frameRate = 30)]
    public class FlashTest extends Sprite {
        public function FlashTest() {
            var rect1: Sprite = DraggableSprite.createRectangle(0x00A0FF, 100, 100, 50, 50)
            var rect2: Sprite = DraggableSprite.createRectangle(0xA0FF00, 300, 200, 100, 20)
            var rect3: Sprite = DraggableSprite.createRectangle(0xFFA000, 100, 300, 70, 40)
            
            addChild(rect1)
            addChild(rect2)
            addChild(rect3)
            
            rect1.rotation += 45
            rect2.rotation += 30
            rect3.rotation += 20

            // リンク設定
            var link1: LinkTest = new LinkTest(rect1, rect2)
            var link2: LinkTest = new LinkTest(rect3, rect2)
            var link3: LinkTest = new LinkTest(rect1, rect3)
            
            // 線の Head / Tail に対する形状設定 ※ 実際は描画用関数自体を設定している。
            with (ArrowShapeUtil) {
                link1.headShapes(triangle, circle, arrow);
                link1.tailShapes(arrow, diamond, arrow);
                link2.headShapes(cross);
                link2.tailShapes(trident, circle);
                link3.headShapes(circle, arrow);
                link3.tailShapes(diamond);
            }
            addChild(link1)
            addChild(link2)
            addChild(link3)
            
            addEventListener(Event.ENTER_FRAME, function (): void {
                rect1.rotation += 1
                rect2.rotation += 3
                rect3.rotation += 2
                
                link1.onRefresh()
                link2.onRefresh()
                link3.onRefresh()
            })
        }
    }
}

import flash.display.Graphics;
import flash.display.DisplayObject;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.MouseEvent;
import flash.geom.Point;

class Bresenham {
    // ブレゼンハムアルゴリズムを使って (x0,y0) から (x1,y1) を走査しながら f() を呼ぶメソッド
    // 引数の f() は f(x, y): Boolean を想定しており true を返されると走査自体を中断する
    // ref: ロジックは http://wonderfl.net/c/gjgO の Bresenham.draw() をベースに作成
    public static function scanLine(x0:int, y0:int, x1:int, y1:int, f: Function): void {
        var steep:Boolean = Math.abs(x1 - x0) < Math.abs(y1 - y0);
        var t:int;
        if (steep) {
            t = x0;
            x0 = y0;
            y0 = t;
            t = x1;
            x1 = y1;
            y1 = t;
        }
        
        var dx:int = Math.abs(x1 - x0);
        var dy:int = Math.abs(y1 - y0);
        var e:int = dx*0.5;
        var ys:int = (y0 < y1) ? 1 : -1;
        var x:int = x0;
        var y:int = y0;
        
        // ループ内の処理は一緒なので関数にまとめている、展開する程の処理は Flash 上で行わないと想定
        function logic(): Boolean {
            if (steep) {
                if (f(y, x)) return true
            } else {
                if (f(x, y)) return true
            }
            e = e - dy;
            if (e < 0) {
                y = y + ys;
                e = e + dx;
            }
            return false
        }
        
        if (x0 <= x1) {
            for (; x <= x1; x++) if (logic()) return
        }
        else {
            for (; x1 < x; x--) if (logic()) return
        }
    }

    public static function contactCheckStartPoint(src: DisplayObject, dst:DisplayObject): Point {
        // とりあえず2点間の中点をチェックの開始位置としておく、簡単な方法で無駄な計算を省いておく。
        return new Point(src.x + (dst.x - src.x) / 2, src.y + (dst.y - src.y) / 2)
    }
    
    // src から dst の中心点を結ぶ線分の dst 上における接点を算出する
    public static function contactPoint(src: DisplayObject, dst:DisplayObject, shapeFlag: Boolean = true): Point {
        var result: Point = null
        var s: Point = contactCheckStartPoint(src, dst);
        scanLine(s.x, s.y, dst.x, dst.y, function(x: int, y: int): Boolean {
            result = new Point(x, y)
            return (dst.hitTestPoint(x, y, shapeFlag));
        });
        return result
    }
}

// 掴める Sprite
class DraggableSprite extends Sprite {
    public static const DRAGGING: String = "dragging"
    public static const COMPLETE: String = "complete"
    public static const REFRESH: String = "refresh"
    
    public static function createRectangle(c: uint, x: Number, y: Number, w: Number, h: Number): Sprite {
        var result: Sprite = new DraggableSprite();
        with (result.graphics) {
            beginFill(c)
            drawRect(- w / 2, - h / 2, w, h)
            endFill()
        }
        
        result.x = x
        result.y = y
        return result
    }
    
    function DraggableSprite() {
        this.addEventListener(MouseEvent.MOUSE_DOWN, function(): void {
            stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove)
            stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp)
        });
    }
    
    private function onMouseMove(e: MouseEvent): void {
        this.x = e.stageX
        this.y = e.stageY
        dispatchEvent(new Event(DRAGGING));
        dispatchEvent(new Event(REFRESH));
    }
    
    private function onMouseUp(e: MouseEvent): void {
        this.stage.removeEventListener(MouseEvent.MOUSE_MOVE, onMouseMove)
        this.stage.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp)
        dispatchEvent(new Event(COMPLETE));
        dispatchEvent(new Event(REFRESH));
    }
}

class ArrowShapeUtil {
    // 点と対象から矢印の描画に必要な向きを求める関数
    private static function delta(p: Point, base: DisplayObject): Point {
        var d: Point = new Point(base.x, base.y).subtract(p)
        d.normalize(1)
        return d
    }
    
    // 関数渡しによる Visitor パターン的なコードテスト
    // これを使って書くと、意図通りに動き、多少コードの簡略化に繋がるかもしれないが、
    // 手続き型に慣れてる人からすると逆に遠回しになり、読みにくいかもしれない。
    private static function wrap(p: Point, base: DisplayObject, f: Function): Point {
        if (null == p) return null;
        var d: Point = delta(p, base)
        return f(d)
    }
    
    // ここから ArrowHead 描画処理の定義
    // 基本的な考えは、接点 p と Arrow の接続対象 base から、向き dp を求める
    // 接点 p を ArrowHead として描画した後、 ArrowTail となる Point を返り値として返す
    // この方法をベースとすれば、デザインパターンにおける Decorator パターンのようにオブジェクト化するのも容易と考える
    public static function circle(g: Graphics, p: Point, base: DisplayObject): Point {
        // 処理をベタに書いてみた場合
        if (null == p) return null
        var dp: Point = delta(p, base)
        
        var radius: int = 5
        var fill: Boolean = false
        var fillColor: uint = 0
        
        var c: Point = new Point(p.x - dp.x * radius, p.y - dp.y * radius)
        if (fill) g.beginFill(fillColor)
        g.drawEllipse(c.x - radius, c.y - radius, radius * 2, radius * 2)
        if (fill) g.endFill()
        return new Point(p.x - dp.x * radius * 2, p.y - dp.y * radius * 2)
    }
    
    public static function arrow(g: Graphics, p: Point, base: DisplayObject): Point {
        // wrap() と無名関数を使った場合
        return wrap(p, base, function(dp: Point): Point { 
            var support: Point = new Point(p.x - dp.x * 20, p.y - dp.y * 20)
            var root: Point = new Point(p.x - dp.x * 15, p.y - dp.y * 15)
            
            g.moveTo(p.x, p.y)
            g.lineTo(support.x + dp.y * 6, support.y - dp.x * 6)
            g.lineTo(root.x, root. y)
            g.lineTo(support.x - dp.y * 6, support.y + dp.x * 6)
            g.lineTo(p.x, p.y)
            return root
        });
    }
    
    public static function triangle(g: Graphics, p: Point, base: DisplayObject): Point {
        // 変数へ function を代入した場合
        var drawTriangle: Function = function(dp: Point): Point {
            var root: Point = new Point(p.x - dp.x * 13, p.y - dp.y * 13)
            g.moveTo(p.x, p.y)
            g.lineTo(root.x + dp.y * 7.5, root.y - dp.x * 7.5)
            g.lineTo(root.x, root. y)
            g.lineTo(root.x - dp.y * 7.5, root.y + dp.x * 7.5)
            g.lineTo(p.x, p.y)
            return root
        }
        // こう書くと解るが単体で使う限り冗長だね。にしても関数型っぽい事が案外簡単にできる
        return wrap(p, base, drawTriangle);
    }
    
    public static function diamond(g: Graphics, p: Point, base: DisplayObject): Point {
        return wrap(p, base, function(dp: Point): Point { 
            var cp: Point = new Point(p.x - dp.x * 10, p.y - dp.y * 10)
            var root: Point = new Point(p.x - dp.x * 20, p.y - dp.y * 20)
            
            g.moveTo(p.x, p.y)
            g.lineTo(cp.x + dp.y * 5, cp.y - dp.x * 5)
            g.lineTo(root.x, root.y)
            g.lineTo(cp.x - dp.y * 5, cp.y + dp.x * 5)
            g.lineTo(p.x, p.y)
            return root
        });
    }
    
    public static function cross(g: Graphics, p: Point, base: DisplayObject): Point {
        return wrap(p, base, function(dp: Point): Point { 
            var cp: Point = new Point(p.x - dp.x * 10, p.y - dp.y * 10)
            var root: Point = new Point(p.x - dp.x * 20, p.y - dp.y * 20)
            
            g.moveTo(cp.x + dp.y * 5, cp.y - dp.x * 5)
            g.lineTo(cp.x - dp.y * 5, cp.y + dp.x * 5)
            
            g.moveTo(p.x, p.y)
            g.lineTo(root.x, root.y)
            return root
        });
    }
    
    public static function trident(g: Graphics, p: Point, base: DisplayObject): Point {
        return wrap(p, base, function(dp: Point): Point { 
            var root: Point = new Point(p.x - dp.x * 10, p.y - dp.y * 10)
            
            g.moveTo(p.x + dp.y * 5, p.y - dp.x * 5)
            g.lineTo(root.x, root.y)
            g.lineTo(p.x - dp.y * 5, p.y + dp.x * 5)
            
            g.moveTo(p.x, p.y)
            g.lineTo(root.x, root.y)
            return root
        });
    }
    // ここまで ArrowHead 描画処理の定義
    
    // 連続的に ArrowHead を描画したい場合の補助メソッドテスト
    // これを使うと with (ArrowShapeUtil) { ..  = draw(graphics, p, base, circle, arrow, ... )  } なんて書ける
    // 各描画処理に対して渡す引数が変わらない場合はこれだけで十分に使えるレベルであるが、凝ったことをしたい場合を考えると
    // ArrowHead 毎にパラメータが変わるだろうから、その場合は使えないか?
    // もし、それをしたい場合は描画関数を渡すのではなく専用の描画用オブジェクトを渡すべきだろうね。
    public static function draw(g: Graphics, p: Point, base: DisplayObject, ... method): Point {
        if (0 == method.length) return p
        if (null == method[0]) return p
        if (method[0] is Array) method = method[0]
        for each (var m: Function in method) p = m(g, p, base)
        return p
    }
}

class LinkTest extends Sprite {
    private var tail: Sprite = null;
    private var head: Sprite = null;
    
    private var _headShapes: Array = null
    private var _tailShapes: Array = null
    
    function LinkTest(aTail: Sprite, aHead: Sprite) {
        link(aTail, aHead)
        
        addEventListener(Event.ADDED, function(e: Event): void {
            onRefresh()
            // removeEventListener(Event.ADDED, arguments.callee)
        })
    }
    
    public function headShapes(... shapes): void { _headShapes = shapes    }
    public function tailShapes(... shapes): void { _tailShapes = shapes    }
    
    public function link(aTail: Sprite, aHead: Sprite): void {
        unlink()
        tail = aTail
        head = aHead
        if (null != tail) tail.addEventListener(DraggableSprite.REFRESH, onRefresh)
        if (null != head) head.addEventListener(DraggableSprite.REFRESH, onRefresh)
    }

    public function unlink(): void {
        if (null != tail) tail.removeEventListener(DraggableSprite.REFRESH, onRefresh)
        if (null != head) head.removeEventListener(DraggableSprite.REFRESH, onRefresh)
        tail = null
        head = null
    }
    
    public function onRefresh(e: Event = null): void {
        graphics.clear()
        graphics.lineStyle(1, 0)
        
        var hp: Point = Bresenham.contactPoint(tail, head)
        var tp: Point = Bresenham.contactPoint(head, tail)
        
        var sp: Point = ArrowShapeUtil.draw(graphics, tp, tail, _tailShapes)
        var ep: Point = ArrowShapeUtil.draw(graphics, hp, head, _headShapes)
        
        graphics.moveTo(sp.x, sp.y)
        graphics.lineTo(ep.x, ep.y)
    }
}